diff --git a/CHANGELOG.md b/CHANGELOG.md index 671f865e780cfb2cb3efe32257b25d9be070bbab..ca4d549091e161fdd2b9441431d973c3f780a6e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.5.0 + +- Add activity parameters widgets + ## 0.4.0 - Add activity actions buttons diff --git a/lib/flutter_toolbox.dart b/lib/flutter_toolbox.dart index 3b348c1ed7a65b47ee5b26884df468b54afa9c79..f4b591f1bb0019f27b912c7ec1db7709f8f2bf96 100644 --- a/lib/flutter_toolbox.dart +++ b/lib/flutter_toolbox.dart @@ -23,6 +23,16 @@ export 'settings/application_settings_theme_card.dart' show ApplicationSettingsT export 'settings/application_theme_mode_cubit.dart' show ApplicationThemeModeCubit, ApplicationThemeModeState; +export 'parameters/application_config_definition.dart' + show + ApplicationConfigDefinition, + ApplicationSettingsParameter, + ApplicationSettingsParameterItemValue; +export 'parameters/pages/parameters.dart' show PageParameters; +export 'parameters/settings/settings_activity_cubit.dart' + show ActivitySettingsCubit, ActivitySettingsState; +export 'parameters/models/settings/settings_activity.dart' show ActivitySettings; + // dependencies export 'package:easy_localization/easy_localization.dart' diff --git a/lib/parameters/application_config_definition.dart b/lib/parameters/application_config_definition.dart new file mode 100644 index 0000000000000000000000000000000000000000..82c0e2e2589a602fede7e68dff317295f79b4d5d --- /dev/null +++ b/lib/parameters/application_config_definition.dart @@ -0,0 +1,156 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_custom_toolbox/flutter_toolbox.dart'; + +class ApplicationConfigDefinition { + final String appTitle; + final List<ApplicationSettingsParameter> activitySettings; + final void Function(BuildContext context) startNewActivity; + final void Function(BuildContext context) deleteCurrentActivity; + final void Function(BuildContext context) resumeActivity; + + const ApplicationConfigDefinition({ + required this.appTitle, + required this.activitySettings, + required this.startNewActivity, + required this.deleteCurrentActivity, + required this.resumeActivity, + }); + + ApplicationSettingsParameter getFromCode(String paramCode) { + final List<ApplicationSettingsParameter> settings = activitySettings; + + return settings.where((setting) => setting.code == paramCode).firstOrNull ?? + ApplicationSettingsParameter.empty(); + } +} + +class ApplicationSettingsParameter { + final String code; + final bool displayedOnTop; + final List<ApplicationSettingsParameterItemValue> values; + final Widget Function({ + required BuildContext context, + required double size, + required ApplicationSettingsParameterItemValue itemValue, + required VoidCallback onPressed, + })? builder; + final CustomPainter Function(BuildContext context, String value)? customPainter; + final int Function(String value)? intValueGetter; + final Color Function(String value)? colorGetter; + + const ApplicationSettingsParameter({ + required this.code, + this.displayedOnTop = true, + required this.values, + this.builder, + this.customPainter, + this.colorGetter, + this.intValueGetter, + }); + + List<String> get allowedValues { + return values.map((ApplicationSettingsParameterItemValue item) => item.value).toList(); + } + + String get defaultValue => values.firstWhere((element) => element.isDefault).value; + + factory ApplicationSettingsParameter.empty() { + return ApplicationSettingsParameter( + code: '', + values: [ + ApplicationSettingsParameterItemValue.empty(), + ], + intValueGetter: (value) => 0, + ); + } + + Widget buildParameterItem({ + required BuildContext context, + required ApplicationSettingsParameter parameter, + required ApplicationSettingsParameterItemValue itemValue, + required double size, + required VoidCallback? onPressed, + }) { + final Color buttonColorActive = (onPressed != null) ? Colors.blue : Colors.grey; + final Color buttonColorInactive = Theme.of(context).colorScheme.surface; + const double buttonBorderWidth = 4.0; + const double buttonBorderRadius = 12.0; + + final ActivitySettingsCubit activitySettingsCubit = + BlocProvider.of<ActivitySettingsCubit>(context); + final String currentValue = activitySettingsCubit.get(code); + + final bool isSelected = (currentValue == itemValue.value); + + final Color buttonColor = isSelected ? buttonColorActive : buttonColorInactive; + + Widget content = SizedBox.shrink(); + + if (builder != null) { + content = builder!( + context: context, + size: size, + itemValue: itemValue, + onPressed: onPressed ?? () {}, + ); + } else { + if (customPainter != null) { + content = StyledButton( + color: itemValue.color ?? Colors.grey, + onPressed: onPressed ?? () {}, + child: CustomPaint( + size: Size(size, size), + willChange: false, + painter: customPainter!(context, itemValue.value), + isComplex: true, + ), + ); + } else { + content = StyledButton.text( + color: itemValue.color ?? Colors.grey, + caption: itemValue.text ?? itemValue.value, + onPressed: onPressed ?? () {}, + ); + } + } + + return Container( + decoration: BoxDecoration( + color: buttonColor, + borderRadius: BorderRadius.circular(buttonBorderRadius), + border: Border.all( + color: buttonColor, + width: buttonBorderWidth, + ), + ), + child: content, + ); + } +} + +class ApplicationSettingsParameterItemValue { + final String value; + final bool isDefault; + final Color? color; + final String? text; + + const ApplicationSettingsParameterItemValue({ + required this.value, + this.isDefault = false, + this.color, + this.text, + }); + + factory ApplicationSettingsParameterItemValue.empty() { + return ApplicationSettingsParameterItemValue( + value: '', + isDefault: true, + ); + } + + @override + String toString() { + return value; + } +} diff --git a/lib/parameters/models/settings/settings_activity.dart b/lib/parameters/models/settings/settings_activity.dart new file mode 100644 index 0000000000000000000000000000000000000000..45b3a12dab7b262f8555358f98a634d5f70d05c7 --- /dev/null +++ b/lib/parameters/models/settings/settings_activity.dart @@ -0,0 +1,73 @@ +import 'package:flutter_custom_toolbox/flutter_toolbox.dart'; + +class ActivitySettings { + final Map<String, String> values; + final ApplicationConfigDefinition appConfig; + + ActivitySettings({ + required this.appConfig, + required this.values, + }); + + factory ActivitySettings.createDefault({ + required ApplicationConfigDefinition appConfig, + }) { + Map<String, String> values = {}; + + for (var setting in appConfig.activitySettings) { + values[setting.code] = setting.defaultValue; + } + + return ActivitySettings( + appConfig: appConfig, + values: values, + ); + } + + String get(String code) { + if (values.keys.contains(code)) { + if (appConfig.getFromCode(code).allowedValues.contains(values[code])) { + return values[code] ?? appConfig.getFromCode(code).defaultValue; + } + } + + return appConfig.getFromCode(code).defaultValue; + } + + int getAsInt(String parameterCode) { + final ApplicationSettingsParameter parameter = appConfig.getFromCode(parameterCode); + + if (values.keys.contains(parameterCode)) { + if (parameter.allowedValues.contains(values[parameterCode])) { + if (parameter.intValueGetter != null) { + return parameter.intValueGetter!(get(parameterCode)); + } else { + return int.parse(get(parameterCode)); + } + } + } + + if (parameter.intValueGetter != null) { + return parameter.intValueGetter!(parameter.defaultValue); + } else { + return int.parse(parameter.defaultValue); + } + } + + void dump() { + printlog('$ActivitySettings:'); + values.forEach((code, value) { + printlog(' $code: $value'); + }); + printlog(''); + } + + @override + String toString() { + return '$ActivitySettings(${toJson()})'; + } + + Map<String, dynamic>? toJson() { + return values; + } +} diff --git a/lib/parameters/pages/parameters.dart b/lib/parameters/pages/parameters.dart new file mode 100644 index 0000000000000000000000000000000000000000..92a065d3779ef68e850929340cebcb57fa2a4e06 --- /dev/null +++ b/lib/parameters/pages/parameters.dart @@ -0,0 +1,137 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_custom_toolbox/flutter_toolbox.dart'; + +class PageParameters extends StatelessWidget { + const PageParameters({ + super.key, + required this.config, + required this.canBeResumed, + }); + + final ApplicationConfigDefinition config; + final bool canBeResumed; + + final double separatorHeight = 8.0; + + @override + Widget build(BuildContext context) { + final List<Widget> lines = []; + + // Activity settings (top) + for (ApplicationSettingsParameter parameter in config.activitySettings) { + if (parameter.displayedOnTop) { + lines.add(Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: buildParametersLine( + parameter: parameter, + isEnabled: !canBeResumed, + ), + )); + + lines.add(SizedBox(height: separatorHeight)); + } + } + + lines.add(Expanded( + child: SizedBox(height: separatorHeight), + )); + + if (canBeResumed) { + // Resume activity + lines.add(AspectRatio( + aspectRatio: 3, + child: ActivityButtonResumeSaved( + onPressed: () { + config.resumeActivity(context); + }, + ), + )); + // Delete saved activity + lines.add(SizedBox.square( + dimension: MediaQuery.of(context).size.width / 5, + child: ActivityButtonDeleteSaved( + onPressed: () { + config.deleteCurrentActivity(context); + }, + ), + )); + } else { + // Start new activity + lines.add( + AspectRatio( + aspectRatio: 3, + child: ActivityButtonStartNew( + onPressed: () { + config.startNewActivity(context); + }, + ), + ), + ); + } + + lines.add(SizedBox(height: separatorHeight)); + + // Activity settings (bottom) + for (ApplicationSettingsParameter parameter in config.activitySettings) { + if (!parameter.displayedOnTop) { + lines.add(Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: buildParametersLine( + parameter: parameter, + isEnabled: !canBeResumed, + ), + )); + + lines.add(SizedBox(height: separatorHeight)); + } + } + + return Column( + children: lines, + ); + } + + List<Widget> buildParametersLine({ + required ApplicationSettingsParameter parameter, + required bool isEnabled, + }) { + final List<ApplicationSettingsParameterItemValue> items = parameter.values; + final List<Widget> parameterButtons = []; + + if (items.length <= 1) { + return []; + } + + for (ApplicationSettingsParameterItemValue item in items) { + final Widget parameterButton = BlocBuilder<ActivitySettingsCubit, ActivitySettingsState>( + builder: (BuildContext context, ActivitySettingsState activitySettingsState) { + final ActivitySettingsCubit activitySettingsCubit = + BlocProvider.of<ActivitySettingsCubit>(context); + + final double displayWidth = MediaQuery.of(context).size.width; + final double itemWidth = displayWidth / items.length - 4; + + return SizedBox.square( + dimension: itemWidth, + child: parameter.buildParameterItem( + context: context, + parameter: parameter, + itemValue: item, + size: itemWidth, + onPressed: isEnabled + ? () { + activitySettingsCubit.set(parameter.code, item.value); + } + : null, + ), + ); + }, + ); + + parameterButtons.add(parameterButton); + } + + return parameterButtons; + } +} diff --git a/lib/parameters/settings/settings_activity_cubit.dart b/lib/parameters/settings/settings_activity_cubit.dart new file mode 100644 index 0000000000000000000000000000000000000000..6123055fdb2bf2f685d28f063dc909ef4d2f6817 --- /dev/null +++ b/lib/parameters/settings/settings_activity_cubit.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_custom_toolbox/flutter_toolbox.dart'; + +part 'settings_activity_state.dart'; + +class ActivitySettingsCubit extends HydratedCubit<ActivitySettingsState> { + final ApplicationConfigDefinition appConfig; + + ActivitySettingsCubit({ + required this.appConfig, + }) : super( + ActivitySettingsState( + settings: ActivitySettings.createDefault( + appConfig: appConfig, + ), + ), + ); + + void setValues({ + Map<String, String>? values, + }) { + emit( + ActivitySettingsState( + settings: ActivitySettings( + appConfig: appConfig, + values: values ?? state.settings.values, + ), + ), + ); + } + + String get(String code) { + return state.settings.get(code); + } + + void set(String code, String value) { + Map<String, String> values = state.settings.values; + + values[code] = value; + + setValues( + values: values, + ); + } + + @override + ActivitySettingsState? fromJson(Map<String, dynamic> json) { + Map<String, String> values = {}; + + json.forEach((key, value) { + values[key] = value as String; + }); + + return ActivitySettingsState( + settings: ActivitySettings( + appConfig: appConfig, + values: values, + ), + ); + } + + @override + Map<String, dynamic>? toJson(ActivitySettingsState state) { + return state.settings.values; + } +} diff --git a/lib/parameters/settings/settings_activity_state.dart b/lib/parameters/settings/settings_activity_state.dart new file mode 100644 index 0000000000000000000000000000000000000000..2b2de42011634e81ae9e6f8bcaa1577f239c778b --- /dev/null +++ b/lib/parameters/settings/settings_activity_state.dart @@ -0,0 +1,15 @@ +part of 'settings_activity_cubit.dart'; + +@immutable +class ActivitySettingsState extends Equatable { + const ActivitySettingsState({ + required this.settings, + }); + + final ActivitySettings settings; + + @override + List<dynamic> get props => <dynamic>[ + settings, + ]; +} diff --git a/pubspec.yaml b/pubspec.yaml index 1074c05786791668740cf5d0cc10cc785bd43020..231b7b68f2f835d5774c630a3df874a0969f1ede 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: "Flutter custom toolbox for org.benoitharrault.* projects." publish_to: "none" -version: 0.4.0 +version: 0.5.0 homepage: https://git.harrault.fr/android/flutter-toolbox