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

Normalize app architecture

parent 64606b97
No related branches found
No related tags found
1 merge request!8Resolve "Normalize game architecture"
Pipeline #5733 passed
Showing
with 617 additions and 140 deletions
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,
];
}
part of 'settings_cubit.dart';
@immutable
class SettingsState extends Equatable {
const SettingsState({
this.dummyValue,
});
final int? dummyValue;
@override
List<dynamic> get props => <dynamic>[
dummyValue,
];
Map<String, dynamic> get values => <String, dynamic>{
'dummyValue': dummyValue,
};
}
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,
];
}
...@@ -2,20 +2,24 @@ import 'dart:io'; ...@@ -2,20 +2,24 @@ import 'dart:io';
import 'package:easy_localization/easy_localization.dart'; 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_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:stopmotion/config/theme.dart'; import 'package:stopmotion/config/theme.dart';
import 'package:stopmotion/cubit/bottom_nav_cubit.dart'; import 'package:stopmotion/cubit/activity_cubit.dart';
import 'package:stopmotion/cubit/settings_cubit.dart'; import 'package:stopmotion/cubit/nav_cubit_pages.dart';
import 'package:stopmotion/cubit/nav_cubit_screens.dart';
import 'package:stopmotion/cubit/settings_global_cubit.dart';
import 'package:stopmotion/cubit/settings_activity_cubit.dart';
import 'package:stopmotion/cubit/theme_cubit.dart';
import 'package:stopmotion/ui/skeleton.dart'; import 'package:stopmotion/ui/skeleton.dart';
void main() async { void main() async {
/// Initialize packages // Initialize packages
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await EasyLocalization.ensureInitialized(); await EasyLocalization.ensureInitialized();
final Directory tmpDir = await getTemporaryDirectory(); final Directory tmpDir = await getTemporaryDirectory();
Hive.init(tmpDir.toString()); Hive.init(tmpDir.toString());
...@@ -23,8 +27,8 @@ void main() async { ...@@ -23,8 +27,8 @@ void main() async {
storageDirectory: tmpDir, storageDirectory: tmpDir,
); );
runApp( SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp])
EasyLocalization( .then((value) => runApp(EasyLocalization(
path: 'assets/translations', path: 'assets/translations',
supportedLocales: const <Locale>[ supportedLocales: const <Locale>[
Locale('en'), Locale('en'),
...@@ -33,8 +37,7 @@ void main() async { ...@@ -33,8 +37,7 @@ void main() async {
fallbackLocale: const Locale('en'), fallbackLocale: const Locale('en'),
useFallbackTranslations: true, useFallbackTranslations: true,
child: const MyApp(), child: const MyApp(),
), )));
);
} }
class MyApp extends StatelessWidget { class MyApp extends StatelessWidget {
...@@ -44,19 +47,31 @@ class MyApp extends StatelessWidget { ...@@ -44,19 +47,31 @@ class MyApp extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MultiBlocProvider( return MultiBlocProvider(
providers: [ providers: [
BlocProvider<BottomNavCubit>(create: (context) => BottomNavCubit()), BlocProvider<NavCubitPage>(create: (context) => NavCubitPage()),
BlocProvider<SettingsCubit>(create: (context) => SettingsCubit()), BlocProvider<NavCubitScreen>(create: (context) => NavCubitScreen()),
BlocProvider<ThemeCubit>(create: (context) => ThemeCubit()),
BlocProvider<ActivityCubit>(create: (context) => ActivityCubit()),
BlocProvider<GlobalSettingsCubit>(create: (context) => GlobalSettingsCubit()),
BlocProvider<ActivitySettingsCubit>(create: (context) => ActivitySettingsCubit()),
], ],
child: MaterialApp( child: BlocBuilder<ThemeCubit, ThemeModeState>(
builder: (BuildContext context, ThemeModeState state) {
return MaterialApp(
title: 'Stop Motion', title: 'Stop Motion',
theme: appTheme,
home: const SkeletonScreen(), home: const SkeletonScreen(),
// Theme stuff
theme: lightTheme,
darkTheme: darkTheme,
themeMode: state.themeMode,
// Localization stuff // Localization stuff
localizationsDelegates: context.localizationDelegates, localizationsDelegates: context.localizationDelegates,
supportedLocales: context.supportedLocales, supportedLocales: context.supportedLocales,
locale: context.locale, locale: context.locale,
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
);
},
), ),
); );
} }
......
import 'package:stopmotion/models/data/picture.dart';
import 'package:stopmotion/models/settings/settings_activity.dart';
import 'package:stopmotion/models/settings/settings_global.dart';
import 'package:stopmotion/utils/tools.dart';
class Activity {
Activity({
// Settings
required this.activitySettings,
required this.globalSettings,
// State
this.isRunning = false,
this.isStarted = false,
this.isFinished = false,
this.animationInProgress = false,
// Base data
this.pictures = const [],
// Activity data
this.position = 1,
});
// Settings
final ActivitySettings activitySettings;
final GlobalSettings globalSettings;
// State
bool isRunning;
bool isStarted;
bool isFinished;
bool animationInProgress;
// Base data
List<Picture> pictures;
// Activity data
int position;
factory Activity.createNull() {
return Activity(
// Settings
activitySettings: ActivitySettings.createDefault(),
globalSettings: GlobalSettings.createDefault(),
// Base data
pictures: [],
);
}
factory Activity.createNew({
ActivitySettings? activitySettings,
GlobalSettings? globalSettings,
}) {
final ActivitySettings newActivitySettings =
activitySettings ?? ActivitySettings.createDefault();
final GlobalSettings newGlobalSettings = globalSettings ?? GlobalSettings.createDefault();
return Activity(
// Settings
activitySettings: newActivitySettings,
globalSettings: newGlobalSettings,
// State
isRunning: true,
// Base data
pictures: [],
);
}
bool get canBeResumed => isStarted && !isFinished;
void dump() {
printlog('');
printlog('## Current activity dump:');
printlog('');
printlog('$Activity:');
printlog(' Settings');
activitySettings.dump();
globalSettings.dump();
printlog(' State');
printlog(' isRunning: $isRunning');
printlog(' isStarted: $isStarted');
printlog(' isFinished: $isFinished');
printlog(' animationInProgress: $animationInProgress');
printlog(' Base data');
printlog(' pictures: $pictures');
printlog(' Activity data');
printlog(' position: $position');
printlog('');
}
@override
String toString() {
return '$Activity(${toJson()})';
}
Map<String, dynamic>? toJson() {
return <String, dynamic>{
// Settings
'activitySettings': activitySettings.toJson(),
'globalSettings': globalSettings.toJson(),
// State
'isRunning': isRunning,
'isStarted': isStarted,
'isFinished': isFinished,
'animationInProgress': animationInProgress,
// Base data
'pictures': pictures,
// Activity data
'position': position,
};
}
}
class Picture {
final String key;
final String file;
const Picture({
required this.key,
required this.file,
});
@override
String toString() {
return '$Picture(${toJson()})';
}
Map<String, dynamic> toJson() {
return {
'key': key,
'file': file,
};
}
}
import 'package:stopmotion/config/default_activity_settings.dart';
import 'package:stopmotion/utils/tools.dart';
class ActivitySettings {
final String movieType;
ActivitySettings({
required this.movieType,
});
static String getMovieTypeValueFromUnsafe(String movieType) {
if (DefaultActivitySettings.allowedMovieTypeValues.contains(movieType)) {
return movieType;
}
return DefaultActivitySettings.defaultMovieTypeValue;
}
factory ActivitySettings.createDefault() {
return ActivitySettings(
movieType: DefaultActivitySettings.defaultMovieTypeValue,
);
}
void dump() {
printlog('$ActivitySettings:');
printlog(' ${DefaultActivitySettings.parameterCodeMovieType}: $movieType');
printlog('');
}
@override
String toString() {
return '$ActivitySettings(${toJson()})';
}
Map<String, dynamic>? toJson() {
return <String, dynamic>{
DefaultActivitySettings.parameterCodeMovieType: movieType,
};
}
}
import 'package:stopmotion/config/default_global_settings.dart';
import 'package:stopmotion/utils/tools.dart';
class GlobalSettings {
String skin;
GlobalSettings({
required this.skin,
});
static String getSkinValueFromUnsafe(String skin) {
if (DefaultGlobalSettings.allowedSkinValues.contains(skin)) {
return skin;
}
return DefaultGlobalSettings.defaultSkinValue;
}
factory GlobalSettings.createDefault() {
return GlobalSettings(
skin: DefaultGlobalSettings.defaultSkinValue,
);
}
void dump() {
printlog('$GlobalSettings:');
printlog(' ${DefaultGlobalSettings.parameterCodeSkin}: $skin');
printlog('');
}
@override
String toString() {
return '$GlobalSettings(${toJson()})';
}
Map<String, dynamic>? toJson() {
return <String, dynamic>{
DefaultGlobalSettings.parameterCodeSkin: skin,
};
}
}
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class AppTitle extends StatelessWidget { class AppHeader extends StatelessWidget {
const AppTitle({super.key, required this.text}); const AppHeader({super.key, required this.text});
final String text; final String text;
...@@ -11,37 +11,22 @@ class AppTitle extends StatelessWidget { ...@@ -11,37 +11,22 @@ class AppTitle extends StatelessWidget {
return Text( return Text(
tr(text), tr(text),
textAlign: TextAlign.start, textAlign: TextAlign.start,
style: Theme.of(context).textTheme.headlineLarge!.apply(fontWeightDelta: 2), style: Theme.of(context).textTheme.headlineMedium!.apply(fontWeightDelta: 2),
); );
} }
} }
class AppTitle1 extends StatelessWidget { class AppTitle extends StatelessWidget {
const AppTitle1({super.key, required this.text}); const AppTitle({super.key, required this.text});
final String text; final String text;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Text( return Text(
text, tr(text),
textAlign: TextAlign.start, textAlign: TextAlign.start,
style: Theme.of(context).textTheme.titleLarge!.apply(fontWeightDelta: 2), style: Theme.of(context).textTheme.titleLarge!.apply(fontWeightDelta: 2),
); );
} }
} }
class AppTitle2 extends StatelessWidget {
const AppTitle2({super.key, required this.text});
final String text;
@override
Widget build(BuildContext context) {
return Text(
text,
textAlign: TextAlign.start,
style: Theme.of(context).textTheme.titleMedium!.apply(fontWeightDelta: 2),
);
}
}
import 'package:flutter/material.dart';
import 'package:stopmotion/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(),
),
],
),
);
}
}
...@@ -2,10 +2,8 @@ import 'package:easy_localization/easy_localization.dart'; ...@@ -2,10 +2,8 @@ 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:stopmotion/cubit/bottom_nav_cubit.dart'; import 'package:stopmotion/config/activity_page.dart';
import 'package:stopmotion/ui/screens/camera.dart'; import 'package:stopmotion/cubit/nav_cubit_pages.dart';
import 'package:stopmotion/ui/screens/settings.dart';
import 'package:stopmotion/ui/screens/home.dart';
class BottomNavBar extends StatelessWidget { class BottomNavBar extends StatelessWidget {
const BottomNavBar({super.key}); const BottomNavBar({super.key});
...@@ -16,36 +14,35 @@ class BottomNavBar extends StatelessWidget { ...@@ -16,36 +14,35 @@ class BottomNavBar extends StatelessWidget {
margin: const EdgeInsets.only(top: 1, right: 4, left: 4), margin: const EdgeInsets.only(top: 1, right: 4, left: 4),
elevation: 4, elevation: 4,
shadowColor: Theme.of(context).colorScheme.shadow, shadowColor: Theme.of(context).colorScheme.shadow,
color: Theme.of(context).colorScheme.surfaceVariant, color: Theme.of(context).colorScheme.surfaceContainerHighest,
shape: const RoundedRectangleBorder( shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only( borderRadius: BorderRadius.only(
topLeft: Radius.circular(16), topLeft: Radius.circular(16),
topRight: Radius.circular(16), topRight: Radius.circular(16),
), ),
), ),
child: BlocBuilder<BottomNavCubit, int>(builder: (BuildContext context, int state) { child: BlocBuilder<NavCubitPage, int>(builder: (BuildContext context, int state) {
final List<ActivityPageItem> pageItems = [
ActivityPage.pageHome,
ActivityPage.pageEditor,
ActivityPage.pagePlayer,
];
final List<BottomNavigationBarItem> items = pageItems.map((ActivityPageItem item) {
return BottomNavigationBarItem(
icon: item.icon,
label: tr(item.code),
);
}).toList();
return BottomNavigationBar( return BottomNavigationBar(
currentIndex: state, currentIndex: state,
onTap: (int index) => context.read<BottomNavCubit>().updateIndex(index), onTap: (int index) => context.read<NavCubitPage>().updateIndex(index),
type: BottomNavigationBarType.fixed, type: BottomNavigationBarType.fixed,
elevation: 0, elevation: 0,
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
selectedItemColor: Theme.of(context).colorScheme.primary, selectedItemColor: Theme.of(context).colorScheme.primary,
unselectedItemColor: Theme.of(context).textTheme.bodySmall!.color, unselectedItemColor: Theme.of(context).textTheme.bodySmall!.color,
items: <BottomNavigationBarItem>[ items: items,
BottomNavigationBarItem(
icon: ScreenHome.navBarIcon,
label: tr(ScreenHome.navBarText),
),
BottomNavigationBarItem(
icon: ScreenCamera.navBarIcon,
label: tr(ScreenCamera.navBarText),
),
BottomNavigationBarItem(
icon: ScreenSettings.navBarIcon,
label: tr(ScreenSettings.navBarText),
),
],
); );
}), }),
); );
......
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:stopmotion/config/screen.dart';
import 'package:stopmotion/cubit/activity_cubit.dart';
import 'package:stopmotion/cubit/nav_cubit_screens.dart';
import 'package:stopmotion/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:stopmotion/cubit/activity_cubit.dart';
import 'package:stopmotion/ui/widgets/take_picture_widget.dart';
class PageEditor extends StatelessWidget {
const PageEditor({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<ActivityCubit, ActivityState>(
builder: (BuildContext context, ActivityState activityState) {
return const Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
SizedBox(height: 8),
Text('[editor]'),
TakePictureWidget(),
],
);
},
);
}
}
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:stopmotion/cubit/activity_cubit.dart';
import 'package:stopmotion/models/activity/activity.dart';
import 'package:stopmotion/ui/helpers/outlined_text_widget.dart';
import 'package:stopmotion/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:stopmotion/cubit/activity_cubit.dart';
import 'package:stopmotion/models/activity/activity.dart';
import 'package:stopmotion/ui/helpers/outlined_text_widget.dart';
import 'package:stopmotion/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:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:stopmotion/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:stopmotion/config/activity_page.dart';
import 'package:stopmotion/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);
},
);
}
}
import 'package:flutter/material.dart';
import 'package:unicons/unicons.dart';
import 'package:stopmotion/ui/widgets/take_picture_widget.dart';
class ScreenCamera extends StatelessWidget {
const ScreenCamera({super.key});
static Icon navBarIcon = const Icon(UniconsLine.camera);
static String navBarText = 'bottom_nav_camera';
@override
Widget build(BuildContext context) {
return Material(
color: Theme.of(context).colorScheme.background,
child: const Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
SizedBox(height: 8),
TakePictureWidget(),
],
),
);
}
}
import 'package:flutter/material.dart';
import 'package:unicons/unicons.dart';
class ScreenHome extends StatelessWidget {
const ScreenHome({super.key});
static Icon navBarIcon = const Icon(UniconsLine.home);
static String navBarText = 'bottom_nav_home';
@override
Widget build(BuildContext context) {
return Material(
color: Theme.of(context).colorScheme.background,
child: ListView(
padding: const EdgeInsets.symmetric(horizontal: 4),
physics: const BouncingScrollPhysics(),
children: const <Widget>[
SizedBox(height: 8),
Text('CONTENT'),
SizedBox(height: 36),
],
),
);
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment