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