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

Merge branch '18-normalize-game-architecture' into 'master'

Resolve "Normalize game architecture"

Closes #18

See merge request !18
parents 50edf648 180d203e
No related branches found
No related tags found
1 merge request!18Resolve "Normalize game architecture"
Pipeline #5816 passed
Showing
with 689 additions and 275 deletions
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:plotter/config/default_global_settings.dart';
import 'package:plotter/models/settings/settings_global.dart';
part 'settings_global_state.dart';
class GlobalSettingsCubit extends HydratedCubit<GlobalSettingsState> {
GlobalSettingsCubit() : super(GlobalSettingsState(settings: GlobalSettings.createDefault()));
void setValues({
String? skin,
String? apiHost,
}) {
emit(
GlobalSettingsState(
settings: GlobalSettings(
skin: skin ?? state.settings.skin,
apiHost: apiHost ?? state.settings.apiHost,
),
),
);
}
String getParameterValue(String code) {
switch (code) {
case DefaultGlobalSettings.parameterCodeSkin:
return GlobalSettings.getSkinValueFromUnsafe(state.settings.skin);
case 'apiHost':
return state.settings.apiHost;
}
return '';
}
void setParameterValue(String code, String value) {
final String skin = (code == DefaultGlobalSettings.parameterCodeSkin)
? value
: getParameterValue(DefaultGlobalSettings.parameterCodeSkin);
final String apiHost = (code == 'apiHost') ? value : getParameterValue('apiHost');
setValues(
skin: skin,
apiHost: apiHost,
);
}
@override
GlobalSettingsState? fromJson(Map<String, dynamic> json) {
final String skin = json[DefaultGlobalSettings.parameterCodeSkin] as String;
final String apiHost = json['apiHost'] as String;
return GlobalSettingsState(
settings: GlobalSettings(
skin: skin,
apiHost: apiHost,
),
);
}
@override
Map<String, dynamic>? toJson(GlobalSettingsState state) {
return <String, dynamic>{
DefaultGlobalSettings.parameterCodeSkin: state.settings.skin,
};
}
}
part of 'settings_global_cubit.dart';
@immutable
class GlobalSettingsState extends Equatable {
const GlobalSettingsState({
required this.settings,
});
final GlobalSettings settings;
@override
List<dynamic> get props => <dynamic>[
settings,
];
}
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
part 'theme_state.dart';
class ThemeCubit extends HydratedCubit<ThemeModeState> {
ThemeCubit() : super(const ThemeModeState());
void getTheme(ThemeModeState state) {
emit(state);
}
@override
ThemeModeState? fromJson(Map<String, dynamic> json) {
switch (json['themeMode']) {
case 'ThemeMode.dark':
return const ThemeModeState(themeMode: ThemeMode.dark);
case 'ThemeMode.light':
return const ThemeModeState(themeMode: ThemeMode.light);
case 'ThemeMode.system':
default:
return const ThemeModeState(themeMode: ThemeMode.system);
}
}
@override
Map<String, String>? toJson(ThemeModeState state) {
return <String, String>{'themeMode': state.themeMode.toString()};
}
}
part of 'theme_cubit.dart';
@immutable
class ThemeModeState extends Equatable {
const ThemeModeState({
this.themeMode,
});
final ThemeMode? themeMode;
@override
List<Object?> get props => <Object?>[
themeMode,
];
}
import 'package:flutter/material.dart';
import 'package:plotter/provider/data.dart';
import 'package:plotter/utils/api.dart';
class Keyboard {
static Container buildWidget(Data myProvider) {
Widget buildKeyWidget(String direction) {
String keyText = '';
String north = '🔼';
String south = '🔽';
String west = '◀️';
String east = '▶️';
if (direction == 'north') {
keyText = north;
} else {
if (direction == 'south') {
keyText = south;
} else {
if (direction == 'west') {
keyText = west;
} else {
if (direction == 'east') {
keyText = east;
}
}
}
}
return TextButton(
style: TextButton.styleFrom(
padding: const EdgeInsets.all(0),
),
child: Text(keyText,
style: const TextStyle(
fontSize: 60.0,
fontWeight: FontWeight.w800,
),
textAlign: TextAlign.center),
onPressed: () {
if (keyText != '') {
Api.move(myProvider, direction);
}
},
);
}
return Container(
margin: const EdgeInsets.symmetric(horizontal: 2),
padding: const EdgeInsets.all(2),
child: Table(
defaultVerticalAlignment: TableCellVerticalAlignment.middle,
children: [
TableRow(children: [
TableCell(child: buildKeyWidget('')),
TableCell(child: buildKeyWidget('north')),
TableCell(child: buildKeyWidget('')),
]),
TableRow(children: [
TableCell(child: buildKeyWidget('west')),
TableCell(child: buildKeyWidget('')),
TableCell(child: buildKeyWidget('east')),
]),
TableRow(children: [
TableCell(child: buildKeyWidget('')),
TableCell(child: buildKeyWidget('south')),
TableCell(child: buildKeyWidget('')),
]),
],
));
}
}
import 'dart:io';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:provider/provider.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hive/hive.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:path_provider/path_provider.dart';
import 'provider/data.dart'; import 'package:plotter/config/theme.dart';
import 'screens/home.dart'; import 'package:plotter/cubit/activity_cubit.dart';
import 'screens/settings.dart'; import 'package:plotter/cubit/nav_cubit_pages.dart';
import 'package:plotter/cubit/nav_cubit_screens.dart';
import 'package:plotter/cubit/settings_global_cubit.dart';
import 'package:plotter/cubit/theme_cubit.dart';
import 'package:plotter/ui/skeleton.dart';
void main() { void main() async {
// Initialize packages
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await EasyLocalization.ensureInitialized();
final Directory tmpDir = await getTemporaryDirectory();
Hive.init(tmpDir.toString());
HydratedBloc.storage = await HydratedStorage.build(
storageDirectory: tmpDir,
);
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]) SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp])
.then((value) => runApp(const MyApp())); .then((value) => runApp(EasyLocalization(
path: 'assets/translations',
supportedLocales: const <Locale>[
Locale('en'),
Locale('fr'),
],
fallbackLocale: const Locale('en'),
useFallbackTranslations: true,
child: const MyApp(),
)));
} }
class MyApp extends StatelessWidget { class MyApp extends StatelessWidget {
...@@ -17,22 +44,33 @@ class MyApp extends StatelessWidget { ...@@ -17,22 +44,33 @@ class MyApp extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ChangeNotifierProvider( return MultiBlocProvider(
create: (BuildContext context) => Data(), providers: [
child: Consumer<Data>(builder: (context, data, child) { BlocProvider<NavCubitPage>(create: (context) => NavCubitPage()),
return MaterialApp( BlocProvider<NavCubitScreen>(create: (context) => NavCubitScreen()),
debugShowCheckedModeBanner: false, BlocProvider<ThemeCubit>(create: (context) => ThemeCubit()),
theme: ThemeData( BlocProvider<ActivityCubit>(create: (context) => ActivityCubit()),
primaryColor: Colors.blue, BlocProvider<GlobalSettingsCubit>(create: (context) => GlobalSettingsCubit()),
visualDensity: VisualDensity.adaptivePlatformDensity, ],
), child: BlocBuilder<ThemeCubit, ThemeModeState>(
home: const Home(), builder: (BuildContext context, ThemeModeState state) {
routes: { return MaterialApp(
Home.id: (context) => const Home(), title: 'Stepper plotter assistant',
SettingsPage.id: (context) => const SettingsPage(), home: const SkeletonScreen(),
},
); // Theme stuff
}), theme: lightTheme,
darkTheme: darkTheme,
themeMode: state.themeMode,
// Localization stuff
localizationsDelegates: context.localizationDelegates,
supportedLocales: context.supportedLocales,
locale: context.locale,
debugShowCheckedModeBanner: false,
);
},
),
); );
} }
} }
import 'package:plotter/models/settings/settings_global.dart';
import 'package:plotter/utils/tools.dart';
class Activity {
Activity({
// Settings
required this.globalSettings,
// State
this.isRunning = false,
// Base data
this.apiHost = '',
// Activity data
this.apiStatus = '',
});
// Settings
final GlobalSettings globalSettings;
// State
bool isRunning;
// Base data
String apiHost;
// Activity data
String apiStatus;
factory Activity.createNull() {
return Activity(
// Settings
globalSettings: GlobalSettings.createDefault(),
// Base data
apiHost: '',
);
}
factory Activity.createNew({
GlobalSettings? globalSettings,
}) {
final GlobalSettings newGlobalSettings = globalSettings ?? GlobalSettings.createDefault();
return Activity(
// Settings
globalSettings: newGlobalSettings,
// State
isRunning: true,
// Base data
apiHost: '',
);
}
void dump() {
printlog('');
printlog('## Current activity dump:');
printlog('');
printlog('$Activity:');
printlog(' Settings');
globalSettings.dump();
printlog(' State');
printlog(' isRunning: $isRunning');
printlog(' Base data');
printlog(' apiHost: $apiHost');
printlog(' Activity data');
printlog(' apiStatus: $apiStatus');
printlog('');
}
@override
String toString() {
return '$Activity(${toJson()})';
}
Map<String, dynamic>? toJson() {
return <String, dynamic>{
// Settings
'globalSettings': globalSettings.toJson(),
// State
'isRunning': isRunning,
// Base data
'apiHost': apiHost,
// Activity data
'apiStatus': apiStatus,
};
}
}
import 'package:plotter/config/default_global_settings.dart';
import 'package:plotter/utils/tools.dart';
class GlobalSettings {
String skin;
String apiHost;
GlobalSettings({
required this.skin,
required this.apiHost,
});
static String getSkinValueFromUnsafe(String skin) {
if (DefaultGlobalSettings.allowedSkinValues.contains(skin)) {
return skin;
}
return DefaultGlobalSettings.defaultSkinValue;
}
factory GlobalSettings.createDefault() {
return GlobalSettings(
skin: DefaultGlobalSettings.defaultSkinValue,
apiHost: '',
);
}
void dump() {
printlog('$GlobalSettings:');
printlog(' ${DefaultGlobalSettings.parameterCodeSkin}: $skin');
printlog(' apiHost: $apiHost');
printlog('');
}
@override
String toString() {
return '$GlobalSettings(${toJson()})';
}
Map<String, dynamic>? toJson() {
return <String, dynamic>{
DefaultGlobalSettings.parameterCodeSkin: skin,
'apiHost': apiHost,
};
}
}
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:plotter/utils/tools.dart';
class Data extends ChangeNotifier {
// Application settings
String _apiHost = '';
String _apiStatus = '';
String defaultApiHost = '127.0.0.1';
String getParameterValue(String parameterCode) {
switch (parameterCode) {
case 'apiHost':
{
return _apiHost;
}
}
return '';
}
setParameterValue(String parameterCode, String parameterValue) async {
printlog('set parameter "$parameterCode" to value "$parameterValue"');
switch (parameterCode) {
case 'apiHost':
{
updateApiHost(parameterValue);
}
break;
}
final prefs = await SharedPreferences.getInstance();
prefs.setString(parameterCode, parameterValue);
}
void initParametersValues() async {
final prefs = await SharedPreferences.getInstance();
setParameterValue('apiHost', prefs.getString('apiHost') ?? defaultApiHost);
}
String get apiHost => _apiHost;
void updateApiHost(String apiHost) {
_apiHost = apiHost;
notifyListeners();
}
String get apiStatus => _apiStatus;
void updateApiStatus(String apiStatus) {
printlog('new API status: $apiStatus');
_apiStatus = apiStatus;
notifyListeners();
}
}
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:plotter/layout/keyboard.dart';
import 'package:plotter/provider/data.dart';
import 'package:plotter/screens/settings.dart';
import 'package:plotter/utils/api.dart';
class Home extends StatefulWidget {
static const String id = 'home';
const Home({super.key});
@override
HomeState createState() => HomeState();
}
class HomeState extends State<Home> {
@override
void initState() {
super.initState();
Data myProvider = Provider.of<Data>(context, listen: false);
myProvider.initParametersValues();
}
@override
Widget build(BuildContext context) {
Data myProvider = Provider.of<Data>(context);
return Scaffold(
appBar: AppBar(
title: const Text('Stepper plotter assistant'),
actions: [
IconButton(
icon: const Icon(Icons.settings),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) {
return const SettingsPage();
}),
);
},
)
],
leading: IconButton(
icon: Image.asset('assets/icons/application.png'),
onPressed: () {},
),
),
body: SafeArea(
child: Center(
child: Column(
children: [
Keyboard.buildWidget(myProvider),
TextButton(
child: const Text('get API status'),
onPressed: () => Api.updateApiStatus(myProvider),
),
Text(myProvider.apiStatus)
],
)),
));
}
}
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:plotter/provider/data.dart';
class SettingsPage extends StatefulWidget {
const SettingsPage({super.key});
static const String id = 'settings';
@override
SettingsPageState createState() => SettingsPageState();
}
class SettingsPageState extends State<SettingsPage> {
TextEditingController apiHostFieldController = TextEditingController();
@override
void dispose() {
apiHostFieldController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
Data myProvider = Provider.of<Data>(context);
apiHostFieldController.text = myProvider.getParameterValue('apiHost');
return Scaffold(
appBar: AppBar(
title: const Text('Stepper - settings'),
actions: [
IconButton(
icon: const Icon(Icons.save),
onPressed: () {
myProvider.setParameterValue('apiHost', apiHostFieldController.text);
Navigator.pop(context);
},
)
],
leading: IconButton(
icon: Image.asset('assets/icons/application.png'),
onPressed: () {},
),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: TextFormField(
controller: apiHostFieldController,
decoration: const InputDecoration(
border: UnderlineInputBorder(),
labelText: 'API Host',
),
),
),
);
}
}
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
class AppHeader extends StatelessWidget {
const AppHeader({super.key, required this.text});
final String text;
@override
Widget build(BuildContext context) {
return Text(
tr(text),
textAlign: TextAlign.start,
style: Theme.of(context).textTheme.headlineMedium!.apply(fontWeightDelta: 2),
);
}
}
class AppTitle extends StatelessWidget {
const AppTitle({super.key, required this.text});
final String text;
@override
Widget build(BuildContext context) {
return Text(
tr(text),
textAlign: TextAlign.start,
style: Theme.of(context).textTheme.titleLarge!.apply(fontWeightDelta: 2),
);
}
}
import 'package:flutter/material.dart';
import 'package:plotter/utils/color_extensions.dart';
class OutlinedText extends StatelessWidget {
const OutlinedText({
super.key,
required this.text,
required this.fontSize,
required this.textColor,
this.outlineColor,
});
final String text;
final double fontSize;
final Color textColor;
final Color? outlineColor;
@override
Widget build(BuildContext context) {
final double delta = fontSize / 30;
return Text(
text,
style: TextStyle(
inherit: true,
fontSize: fontSize,
fontWeight: FontWeight.w600,
color: textColor,
shadows: [
Shadow(
offset: Offset(-delta, -delta),
color: outlineColor ?? textColor.darken(),
),
Shadow(
offset: Offset(delta, -delta),
color: outlineColor ?? textColor.darken(),
),
Shadow(
offset: Offset(delta, delta),
color: outlineColor ?? textColor.darken(),
),
Shadow(
offset: Offset(-delta, delta),
color: outlineColor ?? textColor.darken(),
),
],
),
);
}
}
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:plotter/config/activity_page.dart';
import 'package:plotter/cubit/nav_cubit_pages.dart';
class BottomNavBar extends StatelessWidget {
const BottomNavBar({super.key});
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.only(top: 1, right: 4, left: 4),
elevation: 4,
shadowColor: Theme.of(context).colorScheme.shadow,
color: Theme.of(context).colorScheme.surfaceContainerHighest,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
),
child: BlocBuilder<NavCubitPage, int>(builder: (BuildContext context, int state) {
final List<ActivityPageItem> pageItems = [
ActivityPage.pageHome,
ActivityPage.pageRemoteControl,
];
final List<BottomNavigationBarItem> items = pageItems.map((ActivityPageItem item) {
return BottomNavigationBarItem(
icon: item.icon,
label: tr(item.code),
);
}).toList();
return BottomNavigationBar(
currentIndex: state,
onTap: (int index) => context.read<NavCubitPage>().updateIndex(index),
type: BottomNavigationBarType.fixed,
elevation: 0,
backgroundColor: Colors.transparent,
selectedItemColor: Theme.of(context).colorScheme.primary,
unselectedItemColor: Theme.of(context).textTheme.bodySmall!.color,
items: items,
);
}),
);
}
}
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:plotter/config/screen.dart';
import 'package:plotter/cubit/activity_cubit.dart';
import 'package:plotter/cubit/nav_cubit_screens.dart';
import 'package:plotter/ui/helpers/app_titles.dart';
class GlobalAppBar extends StatelessWidget implements PreferredSizeWidget {
const GlobalAppBar({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<ActivityCubit, ActivityState>(
builder: (BuildContext context, ActivityState activityState) {
return BlocBuilder<NavCubitScreen, int>(
builder: (BuildContext context, int screenIndex) {
final List<Widget> menuActions = [];
if (screenIndex == Screen.indexActivity) {
// go to Settings page
menuActions.add(ElevatedButton(
onPressed: () {
context.read<NavCubitScreen>().goToSettingsPage();
},
style: ElevatedButton.styleFrom(
shape: const CircleBorder(),
),
child: Screen.screenSettings.icon,
));
// go to About page
menuActions.add(ElevatedButton(
onPressed: () {
context.read<NavCubitScreen>().goToAboutPage();
},
style: ElevatedButton.styleFrom(
shape: const CircleBorder(),
),
child: Screen.screenAbout.icon,
));
} else {
// back to Home page
menuActions.add(ElevatedButton(
onPressed: () {
context.read<NavCubitScreen>().goToActivityPage();
},
style: ElevatedButton.styleFrom(
shape: const CircleBorder(),
),
child: Screen.screenActivity.icon,
));
}
return AppBar(
title: const AppHeader(text: 'app_name'),
actions: menuActions,
);
},
);
},
);
}
@override
Size get preferredSize => const Size.fromHeight(50);
}
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:plotter/cubit/activity_cubit.dart';
import 'package:plotter/models/activity/activity.dart';
import 'package:plotter/ui/helpers/outlined_text_widget.dart';
import 'package:plotter/utils/color_extensions.dart';
class PageHome extends StatelessWidget {
const PageHome({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<ActivityCubit, ActivityState>(
builder: (BuildContext context, ActivityState activityState) {
final Activity currentActivity = activityState.currentActivity;
return Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
const SizedBox(height: 8),
OutlinedText(
text: '[home]',
fontSize: 50,
textColor: Colors.blueAccent,
outlineColor: Colors.blueAccent.lighten(20),
),
Text(currentActivity.toString()),
],
);
},
);
}
}
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:plotter/cubit/activity_cubit.dart';
import 'package:plotter/models/activity/activity.dart';
import 'package:plotter/ui/helpers/outlined_text_widget.dart';
import 'package:plotter/utils/color_extensions.dart';
class PagePlayer extends StatelessWidget {
const PagePlayer({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<ActivityCubit, ActivityState>(
builder: (BuildContext context, ActivityState activityState) {
final Activity currentActivity = activityState.currentActivity;
return Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
const SizedBox(height: 8),
OutlinedText(
text: '[player]',
fontSize: 50,
textColor: Colors.blueAccent,
outlineColor: Colors.blueAccent.lighten(20),
),
Text(currentActivity.toString()),
],
);
},
);
}
}
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:plotter/cubit/activity_cubit.dart';
import 'package:plotter/models/activity/activity.dart';
import 'package:plotter/ui/widgets/keyboard.dart';
import 'package:plotter/utils/api.dart';
class PageRemoteControl extends StatelessWidget {
const PageRemoteControl({super.key});
@override
Widget build(BuildContext context) {
return SafeArea(
child: Center(
child: Column(
children: [
const Keyboard(),
TextButton(
child: const Text('get API status'),
onPressed: () {
final ActivityCubit activityCubit = BlocProvider.of<ActivityCubit>(context);
Api.updateApiStatus(activityCubit);
},
),
BlocBuilder<ActivityCubit, ActivityState>(
builder: (BuildContext context, ActivityState activityState) {
final Activity currentActivity = activityState.currentActivity;
return Text(currentActivity.apiStatus);
},
)
],
),
),
);
}
}
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:plotter/ui/helpers/app_titles.dart';
class ScreenAbout extends StatelessWidget {
const ScreenAbout({super.key});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
children: <Widget>[
const SizedBox(height: 8),
const AppTitle(text: 'about_title'),
const Text('about_content').tr(),
FutureBuilder<PackageInfo>(
future: PackageInfo.fromPlatform(),
builder: (context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.done:
return const Text('about_version').tr(
namedArgs: {
'version': snapshot.data!.version,
},
);
default:
return const SizedBox();
}
},
),
],
),
);
}
}
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:plotter/config/activity_page.dart';
import 'package:plotter/cubit/nav_cubit_pages.dart';
class ScreenActivity extends StatelessWidget {
const ScreenActivity({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<NavCubitPage, int>(
builder: (BuildContext context, int pageIndex) {
return ActivityPage.getPageWidget(pageIndex);
},
);
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment