diff --git a/android/gradle.properties b/android/gradle.properties index 2aa2a7ba9299223d13e7b0c4ff3bd3f735ebca5e..d786ccb6f687d13ef64ab84562938745769f4ae6 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,5 +1,5 @@ org.gradle.jvmargs=-Xmx1536M android.useAndroidX=true android.enableJetifier=true -app.versionName=1.0.25 -app.versionCode=26 +app.versionName=1.0.26 +app.versionCode=27 diff --git a/assets/translations/en.json b/assets/translations/en.json index 5159cadefc7c57a7695a4fab93ebd84beb1b7edb..c042445a0b6721bbd268a51ec551919311858151 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -3,8 +3,14 @@ "bottom_nav_sample": "Sample", "bottom_nav_chart": "Graph", + "bottom_nav_settings": "Settings", "bottom_nav_about": "About", + "settings_title": "Settings", + "settings_label_api_url": "API URL:", + "settings_label_security_token": "Security token:", + "settings_button_save": "Save", + "about_title": "About", "about_content": "A random application, for testing purpose only.", diff --git a/assets/translations/fr.json b/assets/translations/fr.json index 9f395345b7abc7a81f441f08ad5f1287d03e556b..484f9137b72e42d572192acba9ba0deaf686e78a 100644 --- a/assets/translations/fr.json +++ b/assets/translations/fr.json @@ -3,8 +3,14 @@ "bottom_nav_sample": "Démo", "bottom_nav_chart": "Graph", + "bottom_nav_settings": "Paramètres", "bottom_nav_about": "À propos", + "settings_title": "Paramètres", + "settings_label_api_url": "URL de l'API :", + "settings_label_security_token": "Jeton de sécurité :", + "settings_button_save": "Enregistrer", + "about_title": "À propos", "about_content": "Application fourre-tout, à des fins de tests uniquement.", diff --git a/lib/activities/ActivityDemoPage.dart b/lib/activities/ActivityDemoPage.dart index 24cf707073e59dea7e6b774598cfd77034d8df82..18e0e0f65473302bcc72fa99f46d7fa2597e8a55 100644 --- a/lib/activities/ActivityDemoPage.dart +++ b/lib/activities/ActivityDemoPage.dart @@ -2,8 +2,10 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:ionicons/ionicons.dart'; + import 'package:random/config/theme.dart'; import 'package:random/cubit/data_cubit.dart'; +import 'package:random/cubit/settings_cubit.dart'; class ActivityDemoPage extends StatelessWidget { const ActivityDemoPage({super.key}); @@ -28,6 +30,8 @@ class ActivityDemoPage extends StatelessWidget { SizedBox(height: 20), persistedCounterBlock(), SizedBox(height: 20), + fakeApiCall(), + SizedBox(height: 20), Text('BOTTOM').tr(), ], ), @@ -73,4 +77,25 @@ class ActivityDemoPage extends StatelessWidget { ), ); } + + Widget fakeApiCall() { + return BlocProvider<SettingsCubit>( + create: (BuildContext context) => SettingsCubit(), + child: BlocBuilder<SettingsCubit, SettingsState>( + builder: (BuildContext context, SettingsState state) { + SettingsCubit settings = BlocProvider.of<SettingsCubit>(context); + + return Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text('apiUrl: ' + settings.getSetting('apiUrl')), + Text('securityToken: ' + settings.getSetting('securityToken')), + Text('unknown: ' + settings.getSetting('unknown', 'undefined')), + ], + ); + }, + ), + ); + } } diff --git a/lib/cubit/bottom_nav_cubit.dart b/lib/cubit/bottom_nav_cubit.dart index eb54062a9da27c975b07df38df69f1cdf1d9b776..cc714ed3e4cbf29cecd0ae0e200f72181c51a9ab 100644 --- a/lib/cubit/bottom_nav_cubit.dart +++ b/lib/cubit/bottom_nav_cubit.dart @@ -7,7 +7,8 @@ class BottomNavCubit extends HydratedCubit<int> { void getDemoPage() => emit(0); void getGraphPage() => emit(1); - void getAboutPage() => emit(2); + void getSettingsPage() => emit(2); + void getAboutPage() => emit(3); @override int? fromJson(Map<String, dynamic> json) { diff --git a/lib/cubit/settings_cubit.dart b/lib/cubit/settings_cubit.dart new file mode 100644 index 0000000000000000000000000000000000000000..8d13db05cfcb2ee127aa6cf6188f15982a8d7c0d --- /dev/null +++ b/lib/cubit/settings_cubit.dart @@ -0,0 +1,46 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:hydrated_bloc/hydrated_bloc.dart'; + +part 'settings_state.dart'; + +class SettingsCubit extends HydratedCubit<SettingsState> { + SettingsCubit() : super(const SettingsState()); + + String getSetting(String key, [String? defaultValue]) { + if (state.values.keys.contains(key)) { + return state.values[key] ?? defaultValue ?? ''; + } + + return defaultValue ?? ''; + } + + void setValues({ + String? apiUrl, + String? securityToken, + }) { + emit(SettingsState( + apiUrl: apiUrl != null ? apiUrl : state.apiUrl, + securityToken: securityToken != null ? securityToken : state.securityToken, + )); + } + + @override + SettingsState? fromJson(Map<String, dynamic> json) { + String apiUrl = json['apiUrl'] as String; + String securityToken = json['securityToken'] as String; + + return SettingsState( + apiUrl: apiUrl, + securityToken: securityToken, + ); + } + + @override + Map<String, String>? toJson(SettingsState state) { + return <String, String>{ + 'apiUrl': state.apiUrl ?? '', + 'securityToken': state.securityToken ?? '', + }; + } +} diff --git a/lib/cubit/settings_state.dart b/lib/cubit/settings_state.dart new file mode 100644 index 0000000000000000000000000000000000000000..f435ed3db87719dc5d3fe3452fb440c27da62fc0 --- /dev/null +++ b/lib/cubit/settings_state.dart @@ -0,0 +1,23 @@ +part of 'settings_cubit.dart'; + +@immutable +class SettingsState extends Equatable { + const SettingsState({ + this.apiUrl, + this.securityToken, + }); + + final String? apiUrl; + final String? securityToken; + + @override + List<String?> get props => <String?>[ + apiUrl, + securityToken, + ]; + + Map<String, String?> get values => <String, String?>{ + 'apiUrl': apiUrl, + 'securityToken': securityToken, + }; +} diff --git a/lib/ui/screens/settings_page.dart b/lib/ui/screens/settings_page.dart new file mode 100644 index 0000000000000000000000000000000000000000..133372a54a9659f2508508a381e3addf4beb2ea0 --- /dev/null +++ b/lib/ui/screens/settings_page.dart @@ -0,0 +1,27 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; + +import 'package:random/ui/widgets/settings_form.dart'; + +class SettingsPage extends StatelessWidget { + const SettingsPage({super.key}); + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: <Widget>[ + SizedBox(height: 50), + Text( + 'settings_title', + textAlign: TextAlign.start, + style: Theme.of(context).textTheme.headlineMedium!.apply(fontWeightDelta: 2), + ).tr(), + SizedBox(height: 8), + SettingsForm(), + ], + ); + } +} diff --git a/lib/ui/screens/skeleton_screen.dart b/lib/ui/screens/skeleton_screen.dart index 4c43b9b17d9a55c530deaa4a85c921fc00fdd7c0..412847e212f29e1505cd30eb7204ab2087b01ba2 100644 --- a/lib/ui/screens/skeleton_screen.dart +++ b/lib/ui/screens/skeleton_screen.dart @@ -4,7 +4,9 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:random/activities/ActivityDemoPage.dart'; import 'package:random/activities/ActivityGraphPage.dart'; import 'package:random/cubit/bottom_nav_cubit.dart'; +import 'package:random/cubit/settings_cubit.dart'; import 'package:random/ui/screens/about_page.dart'; +import 'package:random/ui/screens/settings_page.dart'; import 'package:random/ui/widgets/custom_app_bar.dart'; import 'package:random/ui/widgets/bottom_nav_bar.dart'; @@ -16,23 +18,27 @@ class SkeletonScreen extends StatelessWidget { const List<Widget> pageNavigation = <Widget>[ ActivityDemoPage(), ActivityGraphPage(), + SettingsPage(), AboutPage(), ]; - return BlocProvider<BottomNavCubit>( - create: (BuildContext context) => BottomNavCubit(), - child: Scaffold( - extendBodyBehindAppBar: true, - appBar: const CustomAppBarGone(), - body: BlocBuilder<BottomNavCubit, int>( - builder: (BuildContext context, int state) { - return AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - child: pageNavigation.elementAt(state)); - }, + return BlocProvider<SettingsCubit>( + create: (BuildContext context) => SettingsCubit(), + child: BlocProvider<BottomNavCubit>( + create: (BuildContext context) => BottomNavCubit(), + child: Scaffold( + extendBodyBehindAppBar: true, + appBar: const CustomAppBarGone(), + body: BlocBuilder<BottomNavCubit, int>( + builder: (BuildContext context, int state) { + return AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: pageNavigation.elementAt(state)); + }, + ), + bottomNavigationBar: const BottomNavBar(), + backgroundColor: Theme.of(context).colorScheme.background, ), - bottomNavigationBar: const BottomNavBar(), - backgroundColor: Theme.of(context).colorScheme.background, ), ); } diff --git a/lib/ui/widgets/bottom_nav_bar.dart b/lib/ui/widgets/bottom_nav_bar.dart index 5e6f7106eaa7c68a2b9b85440ae307b3abd8c855..445b4f5ad9bc945bbf2b5a1fd1e02c8a09e24f6d 100644 --- a/lib/ui/widgets/bottom_nav_bar.dart +++ b/lib/ui/widgets/bottom_nav_bar.dart @@ -39,6 +39,10 @@ class BottomNavBar extends StatelessWidget { icon: const Icon(Ionicons.pencil_outline), label: tr('bottom_nav_chart'), ), + BottomNavigationBarItem( + icon: const Icon(Ionicons.settings_outline), + label: tr('bottom_nav_settings'), + ), BottomNavigationBarItem( icon: const Icon(Ionicons.information_circle), label: tr('bottom_nav_about'), diff --git a/lib/ui/widgets/settings_form.dart b/lib/ui/widgets/settings_form.dart new file mode 100644 index 0000000000000000000000000000000000000000..e0a6e853c0968c178fa14e797e59809ff91d81fc --- /dev/null +++ b/lib/ui/widgets/settings_form.dart @@ -0,0 +1,79 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:unicons/unicons.dart'; + +import 'package:random/cubit/bottom_nav_cubit.dart'; +import 'package:random/cubit/settings_cubit.dart'; + +class SettingsForm extends StatefulWidget { + const SettingsForm({super.key}); + + @override + State<SettingsForm> createState() => _SettingsFormState(); +} + +class _SettingsFormState extends State<SettingsForm> { + final apiUrlController = TextEditingController(); + final securityTokenController = TextEditingController(); + + @override + void dispose() { + apiUrlController.dispose(); + securityTokenController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + SettingsCubit settings = BlocProvider.of<SettingsCubit>(context); + + apiUrlController.text = settings.getSetting('apiUrl'); + securityTokenController.text = settings.getSetting('securityToken'); + + void saveSettings() { + BlocProvider.of<SettingsCubit>(context).setValues( + apiUrl: apiUrlController.text, + securityToken: securityTokenController.text, + ); + + BlocProvider.of<BottomNavCubit>(context).getDemoPage(); + } + + return Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: <Widget>[ + Text('settings_label_api_url').tr(), + TextFormField( + controller: apiUrlController, + decoration: InputDecoration( + border: UnderlineInputBorder(), + ), + ), + SizedBox(height: 16), + Text('settings_label_security_token').tr(), + TextFormField( + controller: securityTokenController, + decoration: InputDecoration( + border: UnderlineInputBorder(), + ), + ), + SizedBox(height: 16), + ElevatedButton( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon(UniconsLine.save), + SizedBox(width: 8), + Text('settings_button_save').tr(), + ], + ), + onPressed: () => saveSettings(), + ), + ], + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 814ae8dc78a2e43816019f755a174bd6a6549363..f09ef341ba589f2539fb9e183e59ffe49fff7809 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -325,6 +325,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.2" + unicons: + dependency: "direct main" + description: + name: unicons + sha256: dbfcf93ff4d4ea19b324113857e358e4882115ab85db04417a4ba1c72b17a670 + url: "https://pub.dev" + source: hosted + version: "2.1.1" vector_math: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 8afde1c436f9b90abf159446c23b4d562ad1f333..dfa6e18b1cab9270697726fc6c0da22fc3fc7eb1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: A random application, for testing purpose only. publish_to: 'none' -version: 1.0.25+26 +version: 1.0.26+27 environment: sdk: '^3.0.0' @@ -18,6 +18,7 @@ dependencies: path_provider: ^2.0.11 hydrated_bloc: ^9.0.0 ionicons: ^0.2.2 + unicons: ^2.1.1 flutter: uses-material-design: false