Skip to content
Snippets Groups Projects
Commit 3cd493e2 authored by Benoît Harrault's avatar Benoît Harrault
Browse files

Add "settings" page

parent 2823e0f5
No related branches found
No related tags found
1 merge request!34Resolve "Add settings page"
Pipeline #4537 passed
org.gradle.jvmargs=-Xmx1536M org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true android.useAndroidX=true
android.enableJetifier=true android.enableJetifier=true
app.versionName=1.0.25 app.versionName=1.0.26
app.versionCode=26 app.versionCode=27
...@@ -3,8 +3,14 @@ ...@@ -3,8 +3,14 @@
"bottom_nav_sample": "Sample", "bottom_nav_sample": "Sample",
"bottom_nav_chart": "Graph", "bottom_nav_chart": "Graph",
"bottom_nav_settings": "Settings",
"bottom_nav_about": "About", "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_title": "About",
"about_content": "A random application, for testing purpose only.", "about_content": "A random application, for testing purpose only.",
......
...@@ -3,8 +3,14 @@ ...@@ -3,8 +3,14 @@
"bottom_nav_sample": "Démo", "bottom_nav_sample": "Démo",
"bottom_nav_chart": "Graph", "bottom_nav_chart": "Graph",
"bottom_nav_settings": "Paramètres",
"bottom_nav_about": "À propos", "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_title": "À propos",
"about_content": "Application fourre-tout, à des fins de tests uniquement.", "about_content": "Application fourre-tout, à des fins de tests uniquement.",
......
...@@ -2,8 +2,10 @@ import 'package:easy_localization/easy_localization.dart'; ...@@ -2,8 +2,10 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:ionicons/ionicons.dart'; import 'package:ionicons/ionicons.dart';
import 'package:random/config/theme.dart'; import 'package:random/config/theme.dart';
import 'package:random/cubit/data_cubit.dart'; import 'package:random/cubit/data_cubit.dart';
import 'package:random/cubit/settings_cubit.dart';
class ActivityDemoPage extends StatelessWidget { class ActivityDemoPage extends StatelessWidget {
const ActivityDemoPage({super.key}); const ActivityDemoPage({super.key});
...@@ -28,6 +30,8 @@ class ActivityDemoPage extends StatelessWidget { ...@@ -28,6 +30,8 @@ class ActivityDemoPage extends StatelessWidget {
SizedBox(height: 20), SizedBox(height: 20),
persistedCounterBlock(), persistedCounterBlock(),
SizedBox(height: 20), SizedBox(height: 20),
fakeApiCall(),
SizedBox(height: 20),
Text('BOTTOM').tr(), Text('BOTTOM').tr(),
], ],
), ),
...@@ -73,4 +77,25 @@ class ActivityDemoPage extends StatelessWidget { ...@@ -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')),
],
);
},
),
);
}
} }
...@@ -7,7 +7,8 @@ class BottomNavCubit extends HydratedCubit<int> { ...@@ -7,7 +7,8 @@ class BottomNavCubit extends HydratedCubit<int> {
void getDemoPage() => emit(0); void getDemoPage() => emit(0);
void getGraphPage() => emit(1); void getGraphPage() => emit(1);
void getAboutPage() => emit(2); void getSettingsPage() => emit(2);
void getAboutPage() => emit(3);
@override @override
int? fromJson(Map<String, dynamic> json) { int? fromJson(Map<String, dynamic> json) {
......
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 ?? '',
};
}
}
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,
};
}
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(),
],
);
}
}
...@@ -4,7 +4,9 @@ import 'package:flutter_bloc/flutter_bloc.dart'; ...@@ -4,7 +4,9 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:random/activities/ActivityDemoPage.dart'; import 'package:random/activities/ActivityDemoPage.dart';
import 'package:random/activities/ActivityGraphPage.dart'; import 'package:random/activities/ActivityGraphPage.dart';
import 'package:random/cubit/bottom_nav_cubit.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/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/custom_app_bar.dart';
import 'package:random/ui/widgets/bottom_nav_bar.dart'; import 'package:random/ui/widgets/bottom_nav_bar.dart';
...@@ -16,23 +18,27 @@ class SkeletonScreen extends StatelessWidget { ...@@ -16,23 +18,27 @@ class SkeletonScreen extends StatelessWidget {
const List<Widget> pageNavigation = <Widget>[ const List<Widget> pageNavigation = <Widget>[
ActivityDemoPage(), ActivityDemoPage(),
ActivityGraphPage(), ActivityGraphPage(),
SettingsPage(),
AboutPage(), AboutPage(),
]; ];
return BlocProvider<BottomNavCubit>( return BlocProvider<SettingsCubit>(
create: (BuildContext context) => BottomNavCubit(), create: (BuildContext context) => SettingsCubit(),
child: Scaffold( child: BlocProvider<BottomNavCubit>(
extendBodyBehindAppBar: true, create: (BuildContext context) => BottomNavCubit(),
appBar: const CustomAppBarGone(), child: Scaffold(
body: BlocBuilder<BottomNavCubit, int>( extendBodyBehindAppBar: true,
builder: (BuildContext context, int state) { appBar: const CustomAppBarGone(),
return AnimatedSwitcher( body: BlocBuilder<BottomNavCubit, int>(
duration: const Duration(milliseconds: 300), builder: (BuildContext context, int state) {
child: pageNavigation.elementAt(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,
), ),
); );
} }
......
...@@ -39,6 +39,10 @@ class BottomNavBar extends StatelessWidget { ...@@ -39,6 +39,10 @@ class BottomNavBar extends StatelessWidget {
icon: const Icon(Ionicons.pencil_outline), icon: const Icon(Ionicons.pencil_outline),
label: tr('bottom_nav_chart'), label: tr('bottom_nav_chart'),
), ),
BottomNavigationBarItem(
icon: const Icon(Ionicons.settings_outline),
label: tr('bottom_nav_settings'),
),
BottomNavigationBarItem( BottomNavigationBarItem(
icon: const Icon(Ionicons.information_circle), icon: const Icon(Ionicons.information_circle),
label: tr('bottom_nav_about'), label: tr('bottom_nav_about'),
......
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(),
),
],
);
}
}
...@@ -325,6 +325,14 @@ packages: ...@@ -325,6 +325,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.2" 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: vector_math:
dependency: transitive dependency: transitive
description: description:
......
...@@ -3,7 +3,7 @@ description: A random application, for testing purpose only. ...@@ -3,7 +3,7 @@ description: A random application, for testing purpose only.
publish_to: 'none' publish_to: 'none'
version: 1.0.25+26 version: 1.0.26+27
environment: environment:
sdk: '^3.0.0' sdk: '^3.0.0'
...@@ -18,6 +18,7 @@ dependencies: ...@@ -18,6 +18,7 @@ dependencies:
path_provider: ^2.0.11 path_provider: ^2.0.11
hydrated_bloc: ^9.0.0 hydrated_bloc: ^9.0.0
ionicons: ^0.2.2 ionicons: ^0.2.2
unicons: ^2.1.1
flutter: flutter:
uses-material-design: false uses-material-design: false
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment