Browse Source

neon: Implement Material 3 theme

pull/94/head
jld3103 2 years ago
parent
commit
9e911c48a1
No known key found for this signature in database
GPG Key ID: 9062417B9E8EB7B3
  1. 33
      packages/neon/integration_test/screenshot_test.dart
  2. 1
      packages/neon/lib/l10n/en.arb
  3. 6
      packages/neon/lib/l10n/localizations.dart
  4. 3
      packages/neon/lib/l10n/localizations_en.dart
  5. 51
      packages/neon/lib/src/app.dart
  6. 210
      packages/neon/lib/src/apps/notes/pages/note.dart
  7. 1
      packages/neon/lib/src/apps/notifications/pages/main.dart
  8. 6
      packages/neon/lib/src/pages/home/home.dart
  9. 3
      packages/neon/lib/src/pages/settings/settings.dart
  10. 2
      packages/neon/lib/src/utils/confirmation_dialog.dart
  11. 8
      packages/neon/lib/src/utils/global_options.dart
  12. 113
      packages/neon/lib/src/utils/theme.dart

33
packages/neon/integration_test/screenshot_test.dart

@ -15,6 +15,7 @@ import 'package:neon/src/neon.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:rxdart/rxdart.dart'; import 'package:rxdart/rxdart.dart';
import 'package:settings/settings.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
class MemorySharedPreferences implements SharedPreferences { class MemorySharedPreferences implements SharedPreferences {
@ -169,21 +170,25 @@ Future pumpAppPage(
], ],
child: StreamBuilder<NextcloudTheme>( child: StreamBuilder<NextcloudTheme>(
stream: userThemeStream, stream: userThemeStream,
builder: (final context, final themeSnapshot) => StreamBuilder<ThemeMode>( builder: (final context, final themeSnapshot) => OptionBuilder(
stream: globalOptions.themeMode.stream, option: globalOptions.themeMode,
builder: (final context, final themeModeSnapshot) => StreamBuilder<bool>( builder: (final context, final themeMode) => OptionBuilder(
stream: globalOptions.themeOLEDAsDark.stream, option: globalOptions.themeOLEDAsDark,
builder: (final context, final themeOLEDAsDarkSnapshot) => MaterialApp( builder: (final context, final themeOLEDAsDark) => OptionBuilder(
debugShowCheckedModeBanner: false, option: globalOptions.themeKeepOriginalAccentColor,
localizationsDelegates: AppLocalizations.localizationsDelegates, builder: (final context, final themeKeepOriginalAccentColor) => MaterialApp(
supportedLocales: AppLocalizations.supportedLocales, debugShowCheckedModeBanner: false,
theme: getThemeFromNextcloudTheme( localizationsDelegates: AppLocalizations.localizationsDelegates,
themeSnapshot.data, supportedLocales: AppLocalizations.supportedLocales,
themeModeSnapshot.data ?? ThemeMode.system, theme: getThemeFromNextcloudTheme(
Brightness.light, themeSnapshot.data,
oledAsDark: themeOLEDAsDarkSnapshot.data ?? false, themeMode ?? ThemeMode.system,
Brightness.light,
oledAsDark: themeOLEDAsDark ?? false,
keepOriginalAccentColor: themeKeepOriginalAccentColor ?? true,
),
home: builder(context, userThemeStream.add),
), ),
home: builder(context, userThemeStream.add),
), ),
), ),
), ),

1
packages/neon/lib/l10n/en.arb

@ -80,6 +80,7 @@
"globalOptionsThemeModeDark": "Dark", "globalOptionsThemeModeDark": "Dark",
"globalOptionsThemeModeAutomatic": "Automatic", "globalOptionsThemeModeAutomatic": "Automatic",
"globalOptionsThemeOLEDAsDark": "OLED theme as dark theme", "globalOptionsThemeOLEDAsDark": "OLED theme as dark theme",
"globalOptionsThemeKeepOriginalAccentColor": "Keep the original accent color",
"globalOptionsPushNotificationsNotice": "External services are used for delivering push notifications. While the content is encrypted and can only be read by this app, extracting metadata like the time and count of notifications is still possible.", "globalOptionsPushNotificationsNotice": "External services are used for delivering push notifications. While the content is encrypted and can only be read by this app, extracting metadata like the time and count of notifications is still possible.",
"globalOptionsPushNotificationsEnabled": "Enabled", "globalOptionsPushNotificationsEnabled": "Enabled",
"globalOptionsPushNotificationsEnabledDisabledNotice": "No UnifiedPush distributor could be found. Please go to https://unifiedpush.org/users/distributors and setup any of the listed distributors. Then re-open this app and you should be able to enable notifications", "globalOptionsPushNotificationsEnabledDisabledNotice": "No UnifiedPush distributor could be found. Please go to https://unifiedpush.org/users/distributors and setup any of the listed distributors. Then re-open this app and you should be able to enable notifications",

6
packages/neon/lib/l10n/localizations.dart

@ -401,6 +401,12 @@ abstract class AppLocalizations {
/// **'OLED theme as dark theme'** /// **'OLED theme as dark theme'**
String get globalOptionsThemeOLEDAsDark; String get globalOptionsThemeOLEDAsDark;
/// No description provided for @globalOptionsThemeKeepOriginalAccentColor.
///
/// In en, this message translates to:
/// **'Keep the original accent color'**
String get globalOptionsThemeKeepOriginalAccentColor;
/// No description provided for @globalOptionsPushNotificationsNotice. /// No description provided for @globalOptionsPushNotificationsNotice.
/// ///
/// In en, this message translates to: /// In en, this message translates to:

3
packages/neon/lib/l10n/localizations_en.dart

@ -171,6 +171,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get globalOptionsThemeOLEDAsDark => 'OLED theme as dark theme'; String get globalOptionsThemeOLEDAsDark => 'OLED theme as dark theme';
@override
String get globalOptionsThemeKeepOriginalAccentColor => 'Keep the original accent color';
@override @override
String get globalOptionsPushNotificationsNotice => String get globalOptionsPushNotificationsNotice =>
'External services are used for delivering push notifications. While the content is encrypted and can only be read by this app, extracting metadata like the time and count of notifications is still possible.'; 'External services are used for delivering push notifications. While the content is encrypted and can only be read by this app, extracting metadata like the time and count of notifications is still possible.';

51
packages/neon/lib/src/app.dart

@ -92,29 +92,34 @@ class _NeonAppState extends State<NeonApp> with WidgetsBindingObserver {
@override @override
Widget build(final BuildContext context) => StreamBuilder<Brightness>( Widget build(final BuildContext context) => StreamBuilder<Brightness>(
stream: _platformBrightness, stream: _platformBrightness,
builder: (final context, final platformBrightnessSnapshot) => StreamBuilder<ThemeMode>( builder: (final context, final platformBrightnessSnapshot) => OptionBuilder(
stream: widget.globalOptions.themeMode.stream, option: widget.globalOptions.themeMode,
builder: (final context, final themeModeSnapshot) => StreamBuilder<bool>( builder: (final context, final themeMode) => OptionBuilder(
stream: widget.globalOptions.themeOLEDAsDark.stream, option: widget.globalOptions.themeOLEDAsDark,
builder: (final context, final themeOLEDAsDarkSnapshot) { builder: (final context, final themeOLEDAsDark) => OptionBuilder(
if (!platformBrightnessSnapshot.hasData || option: widget.globalOptions.themeKeepOriginalAccentColor,
!themeOLEDAsDarkSnapshot.hasData || builder: (final context, final themeKeepOriginalAccentColor) {
!themeModeSnapshot.hasData) { if (!platformBrightnessSnapshot.hasData ||
return Container(); themeMode == null ||
} themeOLEDAsDark == null ||
return MaterialApp( themeKeepOriginalAccentColor == null) {
localizationsDelegates: AppLocalizations.localizationsDelegates, return Container();
supportedLocales: AppLocalizations.supportedLocales, }
navigatorKey: _navigatorKey, return MaterialApp(
theme: getThemeFromNextcloudTheme( localizationsDelegates: AppLocalizations.localizationsDelegates,
_userTheme, supportedLocales: AppLocalizations.supportedLocales,
themeModeSnapshot.data!, navigatorKey: _navigatorKey,
platformBrightnessSnapshot.data!, theme: getThemeFromNextcloudTheme(
oledAsDark: themeOLEDAsDarkSnapshot.data!, _userTheme,
), themeMode,
home: Container(), platformBrightnessSnapshot.data!,
); oledAsDark: themeOLEDAsDark,
}, keepOriginalAccentColor: themeKeepOriginalAccentColor,
),
home: Container(),
);
},
),
), ),
), ),
); );

210
packages/neon/lib/src/apps/notes/pages/note.dart

@ -85,120 +85,112 @@ class _NotesNotePageState extends State<NotesNotePage> {
} }
@override @override
Widget build(final BuildContext context) { Widget build(final BuildContext context) => WillPopScope(
final titleInputBorder = UnderlineInputBorder( onWillPop: () async {
borderSide: BorderSide( _update();
color: Theme.of(context).colorScheme.onPrimary,
), if (Provider.of<NeonPlatform>(context, listen: false).canUseWakelock) {
); await Wakelock.disable();
return WillPopScope( }
onWillPop: () async { return true;
_update(); },
child: Scaffold(
if (Provider.of<NeonPlatform>(context, listen: false).canUseWakelock) { resizeToAvoidBottomInset: false,
await Wakelock.disable(); appBar: AppBar(
} titleSpacing: 0,
return true; title: TextField(
}, controller: _titleController,
child: Scaffold( focusNode: _titleFocusNode,
appBar: AppBar( style: const TextStyle(
titleSpacing: 0, fontSize: 22,
title: TextField(
controller: _titleController,
focusNode: _titleFocusNode,
style: TextStyle(
fontSize: 22,
color: Theme.of(context).colorScheme.onPrimary,
),
cursorColor: Theme.of(context).colorScheme.onPrimary,
decoration: InputDecoration(
isDense: true,
contentPadding: EdgeInsets.zero,
border: titleInputBorder,
focusedBorder: titleInputBorder,
),
),
actions: [
IconButton(
icon: Icon(
_synced ? Icons.check : Icons.sync,
), ),
onPressed: _update, decoration: const InputDecoration(
), isDense: true,
IconButton( contentPadding: EdgeInsets.zero,
icon: Icon( border: UnderlineInputBorder(),
_showEditor ? Icons.visibility : Icons.edit, focusedBorder: UnderlineInputBorder(),
), ),
onPressed: () {
setState(() {
_showEditor = !_showEditor;
});
if (_showEditor) {
_focusEditor();
} else {
// Prevent the cursor going back to the title field
_contentFocusNode.unfocus();
_titleFocusNode.unfocus();
}
},
), ),
IconButton( actions: [
onPressed: () async { IconButton(
final result = await showDialog<String>( icon: Icon(
context: context, _synced ? Icons.check : Icons.sync,
builder: (final context) => NotesSelectCategoryDialog( ),
bloc: widget.bloc, onPressed: _update,
note: _note,
),
);
if (result != null) {
_update(result);
}
},
icon: Icon(
MdiIcons.tag,
color: _note.category.isNotEmpty ? NotesCategoryColor.compute(_note.category) : null,
), ),
), IconButton(
], icon: Icon(
), _showEditor ? Icons.visibility : Icons.edit,
body: GestureDetector( ),
onTap: () { onPressed: () {
setState(() { setState(() {
_showEditor = true; _showEditor = !_showEditor;
}); });
}, if (_showEditor) {
child: Container( _focusEditor();
padding: EdgeInsets.symmetric( } else {
vertical: 10, // Prevent the cursor going back to the title field
horizontal: _showEditor ? 20 : 10, _contentFocusNode.unfocus();
), _titleFocusNode.unfocus();
color: Colors.transparent, }
constraints: const BoxConstraints.expand(), },
child: _showEditor ),
? TextField( IconButton(
controller: _contentController, onPressed: () async {
focusNode: _contentFocusNode, final result = await showDialog<String>(
keyboardType: TextInputType.multiline, context: context,
maxLines: null, builder: (final context) => NotesSelectCategoryDialog(
decoration: const InputDecoration( bloc: widget.bloc,
border: InputBorder.none, note: _note,
),
);
if (result != null) {
_update(result);
}
},
icon: Icon(
MdiIcons.tag,
color: _note.category.isNotEmpty ? NotesCategoryColor.compute(_note.category) : null,
),
),
],
),
body: GestureDetector(
onTap: () {
setState(() {
_showEditor = true;
});
},
child: Container(
padding: EdgeInsets.symmetric(
vertical: 10,
horizontal: _showEditor ? 20 : 10,
),
color: Colors.transparent,
constraints: const BoxConstraints.expand(),
child: _showEditor
? TextField(
controller: _contentController,
focusNode: _contentFocusNode,
keyboardType: TextInputType.multiline,
maxLines: null,
decoration: const InputDecoration(
border: InputBorder.none,
),
)
: MarkdownBody(
data: _contentController.text,
onTapLink: (final text, final href, final title) async {
if (href != null) {
await launchUrlString(
href,
mode: LaunchMode.externalApplication,
);
}
},
), ),
) ),
: MarkdownBody(
data: _contentController.text,
onTapLink: (final text, final href, final title) async {
if (href != null) {
await launchUrlString(
href,
mode: LaunchMode.externalApplication,
);
}
},
),
), ),
), ),
), );
);
}
} }

1
packages/neon/lib/src/apps/notifications/pages/main.dart

@ -112,6 +112,7 @@ class _NotificationsMainPageState extends State<NotificationsMainPage> {
ElevatedButton( ElevatedButton(
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.red, backgroundColor: Colors.red,
foregroundColor: Theme.of(context).colorScheme.onPrimary,
), ),
onPressed: () { onPressed: () {
Navigator.of(context).pop(); Navigator.of(context).pop();

6
packages/neon/lib/src/pages/home/home.dart

@ -280,6 +280,7 @@ class _HomePageState extends State<HomePage> with tray.TrayListener, WindowListe
ElevatedButton( ElevatedButton(
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.red, backgroundColor: Colors.red,
foregroundColor: Theme.of(context).colorScheme.onPrimary,
), ),
onPressed: () { onPressed: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
@ -427,7 +428,7 @@ class _HomePageState extends State<HomePage> with tray.TrayListener, WindowListe
} }
return DrawerHeader( return DrawerHeader(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).appBarTheme.backgroundColor, color: Theme.of(context).colorScheme.primary,
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -461,7 +462,7 @@ class _HomePageState extends State<HomePage> with tray.TrayListener, WindowListe
child: DropdownButton<String>( child: DropdownButton<String>(
isExpanded: true, isExpanded: true,
dropdownColor: Theme.of(context).colorScheme.primary, dropdownColor: Theme.of(context).colorScheme.primary,
iconEnabledColor: Theme.of(context).appBarTheme.foregroundColor, iconEnabledColor: Theme.of(context).colorScheme.onBackground,
value: widget.account.id, value: widget.account.id,
items: accounts items: accounts
.map<DropdownMenuItem<String>>( .map<DropdownMenuItem<String>>(
@ -607,6 +608,7 @@ class _HomePageState extends State<HomePage> with tray.TrayListener, WindowListe
); );
return Scaffold( return Scaffold(
resizeToAvoidBottomInset: false,
body: Row( body: Row(
children: [ children: [
if (navigationMode == NavigationMode.drawerAlwaysVisible) ...[ if (navigationMode == NavigationMode.drawerAlwaysVisible) ...[

3
packages/neon/lib/src/pages/settings/settings.dart

@ -105,6 +105,9 @@ class _SettingsPageState extends State<SettingsPage> {
CheckBoxSettingsTile( CheckBoxSettingsTile(
option: globalOptions.themeOLEDAsDark, option: globalOptions.themeOLEDAsDark,
), ),
CheckBoxSettingsTile(
option: globalOptions.themeKeepOriginalAccentColor,
),
], ],
), ),
SettingsCategory( SettingsCategory(

2
packages/neon/lib/src/utils/confirmation_dialog.dart

@ -10,6 +10,7 @@ Future<bool> showConfirmationDialog(final BuildContext context, final String tit
ElevatedButton( ElevatedButton(
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.red, backgroundColor: Colors.red,
foregroundColor: Theme.of(context).colorScheme.onPrimary,
), ),
onPressed: () { onPressed: () {
Navigator.of(context).pop(false); Navigator.of(context).pop(false);
@ -19,6 +20,7 @@ Future<bool> showConfirmationDialog(final BuildContext context, final String tit
ElevatedButton( ElevatedButton(
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.green, backgroundColor: Colors.green,
foregroundColor: Theme.of(context).colorScheme.onPrimary,
), ),
onPressed: () { onPressed: () {
Navigator.of(context).pop(true); Navigator.of(context).pop(true);

8
packages/neon/lib/src/utils/global_options.dart

@ -60,6 +60,7 @@ class GlobalOptions {
late final List<Option> options = [ late final List<Option> options = [
themeMode, themeMode,
themeOLEDAsDark, themeOLEDAsDark,
themeKeepOriginalAccentColor,
pushNotificationsEnabled, pushNotificationsEnabled,
pushNotificationsDistributor, pushNotificationsDistributor,
startupMinimized, startupMinimized,
@ -135,6 +136,13 @@ class GlobalOptions {
enabled: _themeOLEDAsDarkEnabledSubject, enabled: _themeOLEDAsDarkEnabledSubject,
); );
late final themeKeepOriginalAccentColor = ToggleOption(
storage: _storage,
key: 'theme-keep-original-accent-color',
label: (final context) => AppLocalizations.of(context).globalOptionsThemeKeepOriginalAccentColor,
defaultValue: BehaviorSubject.seeded(false),
);
late final pushNotificationsEnabled = ToggleOption( late final pushNotificationsEnabled = ToggleOption(
storage: _storage, storage: _storage,
key: 'push-notifications-enabled', key: 'push-notifications-enabled',

113
packages/neon/lib/src/utils/theme.dart

@ -1,20 +1,15 @@
part of '../neon.dart'; part of '../neon.dart';
const themePrimaryColor = Color(0xFFF37736); const themePrimaryColor = Color(0xFFF37736);
const themeOnPrimaryColor = Color(0xFFFFFFFF);
ThemeData getThemeFromNextcloudTheme( ThemeData getThemeFromNextcloudTheme(
final CoreServerCapabilities_Ocs_Data_Capabilities_Theming? nextcloudTheme, final CoreServerCapabilities_Ocs_Data_Capabilities_Theming? nextcloudTheme,
final ThemeMode themeMode, final ThemeMode themeMode,
final Brightness platformBrightness, { final Brightness platformBrightness, {
required final bool oledAsDark, required final bool oledAsDark,
required final bool keepOriginalAccentColor,
}) { }) {
var primaryColor = themePrimaryColor; final primaryColor = nextcloudTheme != null ? HexColor(nextcloudTheme.color) : themePrimaryColor;
var onPrimaryColor = themeOnPrimaryColor;
if (nextcloudTheme != null) {
primaryColor = HexColor(nextcloudTheme.color);
onPrimaryColor = HexColor(nextcloudTheme.colorText);
}
late final Brightness selectBrightness; late final Brightness selectBrightness;
switch (themeMode) { switch (themeMode) {
@ -29,62 +24,24 @@ ThemeData getThemeFromNextcloudTheme(
break; break;
} }
final backgroundColor = selectBrightness == Brightness.dark final oledBackgroundOverride = selectBrightness == Brightness.dark && oledAsDark ? Colors.black : null;
? oledAsDark final colorScheme = ColorScheme.fromSeed(
? Colors.black seedColor: primaryColor,
: const Color(0xFF303030) brightness: selectBrightness,
: Colors.white; ).copyWith(
background: oledBackgroundOverride,
final onBackgroundColor = selectBrightness == Brightness.dark ? Colors.white : const Color(0xFF303030); surface: oledBackgroundOverride,
primary: keepOriginalAccentColor ? primaryColor : null,
final canvasColor = selectBrightness == Brightness.dark
? oledAsDark
? const Color(0xFF202020)
: const Color(0xFF404040)
: const Color(0xFFEAEAEA);
final disabledColor = selectBrightness == Brightness.dark
? oledAsDark
? Colors.grey[700]
: Colors.grey[600]
: Colors.grey[500];
final colorScheme =
(selectBrightness == Brightness.dark ? const ColorScheme.dark() : const ColorScheme.light()).copyWith(
primary: primaryColor,
onPrimary: onPrimaryColor,
secondary: primaryColor,
onSecondary: onPrimaryColor,
background: backgroundColor,
onBackground: onBackgroundColor,
); );
return ThemeData( return ThemeData(
useMaterial3: true, useMaterial3: true,
disabledColor: disabledColor,
brightness: selectBrightness,
scaffoldBackgroundColor: backgroundColor,
canvasColor: canvasColor,
cardColor: backgroundColor,
colorScheme: colorScheme, colorScheme: colorScheme,
textSelectionTheme: TextSelectionThemeData( scaffoldBackgroundColor: colorScheme.background,
cursorColor: primaryColor, canvasColor: colorScheme.background, // For Drawer
selectionColor: primaryColor, cardColor: colorScheme.background, // For LicensePage
selectionHandleColor: primaryColor, snackBarTheme: const SnackBarThemeData(
),
appBarTheme: AppBarTheme(
backgroundColor: primaryColor,
foregroundColor: selectBrightness == Brightness.dark ? Colors.white : Colors.black,
),
snackBarTheme: SnackBarThemeData(
behavior: SnackBarBehavior.floating, behavior: SnackBarBehavior.floating,
actionTextColor: primaryColor,
),
progressIndicatorTheme: ProgressIndicatorThemeData(
color: primaryColor,
),
drawerTheme: DrawerThemeData(
backgroundColor: backgroundColor,
), ),
checkboxTheme: CheckboxThemeData( checkboxTheme: CheckboxThemeData(
fillColor: MaterialStateProperty.resolveWith((final states) { fillColor: MaterialStateProperty.resolveWith((final states) {
@ -92,43 +49,21 @@ ThemeData getThemeFromNextcloudTheme(
return selectBrightness == Brightness.dark ? Colors.white38 : Colors.black38; return selectBrightness == Brightness.dark ? Colors.white38 : Colors.black38;
} }
return primaryColor; return colorScheme.primary;
}), }),
checkColor: MaterialStateProperty.resolveWith((final states) => onPrimaryColor), checkColor: MaterialStateProperty.resolveWith((final states) => colorScheme.onPrimary),
), ),
dividerTheme: DividerThemeData( dividerTheme: const DividerThemeData(
color: primaryColor,
thickness: 1.5, thickness: 1.5,
space: 40, space: 30,
indent: 10,
endIndent: 10,
),
scrollbarTheme: ScrollbarThemeData(
thumbColor: MaterialStateProperty.resolveWith(
(final states) => primaryColor
.withOpacity(states.contains(MaterialState.hovered) || states.contains(MaterialState.dragged) ? 1 : 0.5),
),
mainAxisMargin: 10,
crossAxisMargin: 5,
),
radioTheme: RadioThemeData(
fillColor: MaterialStateProperty.resolveWith(
(final states) => states.contains(MaterialState.disabled) ? disabledColor : primaryColor,
),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
foregroundColor: onPrimaryColor,
backgroundColor: primaryColor,
).copyWith(
elevation: ButtonStyleButton.allOrNull(0),
),
), ),
popupMenuTheme: PopupMenuThemeData( popupMenuTheme: PopupMenuThemeData(
color: canvasColor, // TODO: Only needed until M3 popup menus are implemented
), color: selectBrightness == Brightness.dark
floatingActionButtonTheme: FloatingActionButtonThemeData( ? oledAsDark
backgroundColor: primaryColor, ? const Color(0xFF202020)
: const Color(0xFF404040)
: const Color(0xFFEAEAEA),
), ),
); );
} }

Loading…
Cancel
Save