From 5c5a46fafa31ffd31a6a63ad66eb2a0ec1e7846e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Harrault?= <benoit@harrault.fr> Date: Tue, 26 Nov 2024 16:30:59 +0100 Subject: [PATCH] Add Activity Parameters widgets --- CHANGELOG.md | 4 + lib/flutter_toolbox.dart | 10 ++ .../application_config_definition.dart | 156 ++++++++++++++++++ .../models/settings/settings_activity.dart | 73 ++++++++ lib/parameters/pages/parameters.dart | 137 +++++++++++++++ .../settings/settings_activity_cubit.dart | 67 ++++++++ .../settings/settings_activity_state.dart | 15 ++ pubspec.yaml | 2 +- 8 files changed, 463 insertions(+), 1 deletion(-) create mode 100644 lib/parameters/application_config_definition.dart create mode 100644 lib/parameters/models/settings/settings_activity.dart create mode 100644 lib/parameters/pages/parameters.dart create mode 100644 lib/parameters/settings/settings_activity_cubit.dart create mode 100644 lib/parameters/settings/settings_activity_state.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 671f865..ca4d549 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 3b348c1..f4b591f 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 0000000..82c0e2e --- /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 0000000..45b3a12 --- /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 0000000..92a065d --- /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 0000000..6123055 --- /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 0000000..2b2de42 --- /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 1074c05..231b7b6 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 -- GitLab