From 9f803ae8a0eabc4e768e7e155872a136d4dc0f3b Mon Sep 17 00:00:00 2001 From: Chen Asraf Date: Thu, 11 Apr 2024 00:09:40 +0300 Subject: [PATCH] feat: settings updates, auth post send --- lib/core/features/profile.dart | 7 +- lib/core/profile_presets.dart | 1 + lib/core/store.dart | 4 + lib/core/string_utils.dart | 3 + lib/core/theme.dart | 28 ++++ lib/main.dart | 19 +-- lib/pages/about_page.dart | 5 +- lib/pages/profile_page.dart | 44 +++-- lib/pages/settings_page.dart | 292 +++++++++++++++++++++------------ 9 files changed, 262 insertions(+), 141 deletions(-) create mode 100644 lib/core/theme.dart diff --git a/lib/core/features/profile.dart b/lib/core/features/profile.dart index bf61298..911820e 100644 --- a/lib/core/features/profile.dart +++ b/lib/core/features/profile.dart @@ -18,6 +18,7 @@ class MUDProfile extends PluginBase { String username; String password; AuthMethod authMethod; + bool authPostSend; Settings settings = Settings.empty(); KeyboardShortcutMap keyboardShortcuts = KeyboardShortcutMap.empty(); @@ -36,6 +37,7 @@ class MUDProfile extends PluginBase { this.username = '', this.password = '', this.authMethod = AuthMethod.none, + this.authPostSend = false, }) : _storage = ProfileStorage(id); factory MUDProfile.empty() => MUDProfile( @@ -54,6 +56,7 @@ class MUDProfile extends PluginBase { String? username, String? password, AuthMethod? authMethod, + bool? authPostSend, }) => MUDProfile( id: id ?? this.id, @@ -64,6 +67,7 @@ class MUDProfile extends PluginBase { username: username ?? this.username, password: password ?? this.password, authMethod: authMethod ?? this.authMethod, + authPostSend: authPostSend ?? this.authPostSend, ); factory MUDProfile.fromJson(Map json) { @@ -80,6 +84,7 @@ class MUDProfile extends PluginBase { (e) => e.name == json['authMethod'], orElse: () => AuthMethod.none, ), + authPostSend: json['authPostSend'] ?? false, ); } @@ -92,6 +97,7 @@ class MUDProfile extends PluginBase { 'username': username, 'password': encrypt(password), 'authMethod': authMethod.name, + 'authPostSend': authPostSend, }; @override @@ -192,7 +198,6 @@ class MUDProfile extends PluginBase { return password; } } - } enum AuthMethod { diff --git a/lib/core/profile_presets.dart b/lib/core/profile_presets.dart index 7d68c46..5199d98 100644 --- a/lib/core/profile_presets.dart +++ b/lib/core/profile_presets.dart @@ -21,6 +21,7 @@ final profilePresets = [ host: 'aardmud.org', port: 23, authMethod: AuthMethod.diku, + authPostSend: true, ), MUDProfile( id: 'batmud', diff --git a/lib/core/store.dart b/lib/core/store.dart index e9dad34..1b11386 100644 --- a/lib/core/store.dart +++ b/lib/core/store.dart @@ -120,6 +120,10 @@ class GameStore extends ChangeNotifier { send(currentProfile.username); await Future.delayed(const Duration(milliseconds: 100)); send(currentProfile.password); + if (currentProfile.authPostSend) { + await Future.delayed(const Duration(milliseconds: 100)); + send(''); + } break; case AuthMethod.none: break; diff --git a/lib/core/string_utils.dart b/lib/core/string_utils.dart index decfaef..80dd1cb 100644 --- a/lib/core/string_utils.dart +++ b/lib/core/string_utils.dart @@ -1,3 +1,4 @@ +import 'package:flutter/cupertino.dart'; import 'package:uuid/uuid.dart'; const _uuid = Uuid(); @@ -22,7 +23,9 @@ extension StringExtension on String { String capitalize() { return _capitalize(this); } + String trimMultiline() { return split('\n').map((e) => e.trim()).join('\n').trim(); } } + diff --git a/lib/core/theme.dart b/lib/core/theme.dart new file mode 100644 index 0000000..c65f2a7 --- /dev/null +++ b/lib/core/theme.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; + +ThemeData createTheme({ + Brightness? brightness, + required Color seedColor, +}) { + final base = ThemeData( + colorScheme: ColorScheme.fromSeed( + seedColor: seedColor, + brightness: Brightness.dark, + ), + useMaterial3: true, + ); + + final theme = ThemeData( + colorScheme: ColorScheme.fromSeed( + seedColor: seedColor, + brightness: Brightness.dark, + ), + useMaterial3: true, + listTileTheme: base.listTileTheme.copyWith( + selectedTileColor: base.colorScheme.surfaceVariant, + ), + ); + + return theme; +} + diff --git a/lib/main.dart b/lib/main.dart index b2f8648..4481408 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -7,6 +7,7 @@ import 'core/platform_utils.dart'; import 'core/routes.dart'; import 'core/storage/shared_prefs.dart'; import 'core/store.dart'; +import 'core/theme.dart'; import 'core/window_manager.dart'; void main() async { @@ -28,22 +29,8 @@ class MudblockApp extends StatelessWidget { @override Widget build(BuildContext context) { const baseColor = Colors.blueGrey; - - final darkTheme = ThemeData( - colorScheme: ColorScheme.fromSeed( - seedColor: baseColor, - brightness: Brightness.dark, - ), - useMaterial3: true, - ); - // ignore: unused_local_variable - final lightTheme = ThemeData( - colorScheme: ColorScheme.fromSeed( - seedColor: baseColor, - brightness: Brightness.light, - ), - useMaterial3: true, - ); + final darkTheme = + createTheme(seedColor: baseColor, brightness: Brightness.dark); return MaterialApp( title: 'Mudblock', diff --git a/lib/pages/about_page.dart b/lib/pages/about_page.dart index 219b3e3..6a81c65 100644 --- a/lib/pages/about_page.dart +++ b/lib/pages/about_page.dart @@ -32,6 +32,7 @@ class AboutPage extends StatelessWidget { ), ), ); + final width = MediaQuery.of(context).size.width; return Scaffold( appBar: AppBar( title: const Text('About Mudblock'), @@ -41,12 +42,12 @@ class AboutPage extends StatelessWidget { width: 800, child: ListView( children: [ - if (MediaQuery.of(context).size.width <= 600) ...[ + if (width <= 600) ...[ logo, title, version, ], - if (MediaQuery.of(context).size.width > 600) ...[ + if (width > 600) ...[ Row( mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.max, diff --git a/lib/pages/profile_page.dart b/lib/pages/profile_page.dart index e217c97..435cd86 100644 --- a/lib/pages/profile_page.dart +++ b/lib/pages/profile_page.dart @@ -109,6 +109,29 @@ class _ProfilePageState extends State { 'Authentication', style: Theme.of(context).textTheme.titleMedium, ), + Padding( + padding: const EdgeInsets.only(top: 24), + child: DropdownMenu( + label: const Text('Authentication Method'), + initialSelection: profile.authMethod, + onSelected: (value) { + setDirty(); + setState(() { + profile.authMethod = value as AuthMethod; + }); + }, + dropdownMenuEntries: const [ + DropdownMenuEntry( + value: AuthMethod.none, + label: 'None', + ), + DropdownMenuEntry( + value: AuthMethod.diku, + label: 'Diku-style', + ), + ], + ), + ), TextField( controller: TextEditingController(text: profile.username), decoration: const InputDecoration( @@ -131,25 +154,16 @@ class _ProfilePageState extends State { }, ), const SizedBox(height: 24), - DropdownMenu( - label: const Text('Authentication Method'), - initialSelection: profile.authMethod, - onSelected: (value) { + CheckboxListTile.adaptive( + value: profile.authPostSend, + title: const Text('Send an empty command after logging in'), + subtitle: const Text('Useful for servers that have an MOTD or require login confirmation.'), + onChanged: (value) { setDirty(); setState(() { - profile.authMethod = value as AuthMethod; + profile.authPostSend = value ?? false; }); }, - dropdownMenuEntries: const [ - DropdownMenuEntry( - value: AuthMethod.none, - label: 'None', - ), - DropdownMenuEntry( - value: AuthMethod.diku, - label: 'Diku-style', - ), - ], ), ], ), diff --git a/lib/pages/settings_page.dart b/lib/pages/settings_page.dart index 6277b9c..b83296a 100644 --- a/lib/pages/settings_page.dart +++ b/lib/pages/settings_page.dart @@ -22,127 +22,44 @@ class SettingsPage extends StatefulWidget { class _SettingsPageState extends State with GameStoreStateMixin { late Settings? settings; late GlobalSettings globalSettings; + late PageController pageController; @override void initState() { super.initState(); settings = widget.settings?.copyWith(); globalSettings = widget.globalSettings.copyWith(); + pageController = PageController(initialPage: 0); + pageController.addListener(_notify); + // pageController.animateToPage(0, duration: Duration.zero, curve: Curves.easeInOut); + // HACK otherwise build is not notified of pageController clients + Future.delayed(const Duration(milliseconds: 300)).then((_) => _notify()); + } + + void _notify() { + setState(() {}); + } + + @override + void dispose() { + pageController.dispose(); + super.dispose(); } @override Widget build(BuildContext context) { - final baseFontSize = Theme.of(context).textTheme.bodyText1!.fontSize!; + final width = MediaQuery.of(context).size.width; + debugPrint('clients: ${pageController.hasClients}'); return Scaffold( appBar: AppBar( title: const Text('Settings'), ), - body: Center( - child: SizedBox( - width: 600, - child: ListView( - children: [ - if (settings != null) ...[ - Text('Profile Settings', - style: Theme.of(context).textTheme.titleMedium), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: TextFormField( - initialValue: settings!.commandSeparator, - onChanged: (value) => settings!.commandSeparator = value, - maxLength: 1, - decoration: const InputDecoration( - labelText: 'Command Separator', - helperText: - 'The character that separates commands.\nTo send it literally, use it twice.', - ), - ), - ), - const SizedBox(height: 16), - CheckboxListTile.adaptive( - value: settings!.echoCommands, - onChanged: (value) => - setState(() => settings!.echoCommands = value!), - title: const Text('Echo Commands'), - subtitle: const Text( - 'Whether to echo commands to the screen as they are sent.', - ), - ), - CheckboxListTile.adaptive( - value: settings!.showTimestamps, - onChanged: (value) => - setState(() => settings!.showTimestamps = value!), - title: const Text('Show Timestamps'), - subtitle: const Text( - 'Whether to show timestamps on messages received from the server.', - ), - ), - const SizedBox(height: 16), - ], - Text('Global Settings', - style: Theme.of(context).textTheme.titleMedium), - CheckboxListTile.adaptive( - value: globalSettings.keepAwake, - onChanged: (value) => - setState(() => globalSettings.keepAwake = value!), - title: const Text('Keep Screen Awake'), - subtitle: const Text( - 'Enabling this will make sure the screen doesn\'t turn off while a session is running.', - ), - ), - ListTile( - title: const Text('UI Font Size'), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text('Change the font size of the entire UI.'), - Row( - children: [ - Expanded( - child: Slider( - value: globalSettings.uiTextScale, - onChanged: (value) => setState( - () => globalSettings.uiTextScale = value), - min: 0.5, - max: 2.0, - divisions: 15, - ), - ), - Text( - '${(globalSettings.uiTextScale * baseFontSize).toStringAsFixed(0)}pt (${(globalSettings.uiTextScale * 100).toStringAsFixed(0)}%)'), - ], - ), - ], - ), - ), - ListTile( - title: const Text('Game Output Font Size'), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text('Change the font size of the game output text.'), - Row( - children: [ - Expanded( - child: Slider( - value: globalSettings.gameTextScale, - onChanged: (value) => setState( - () => globalSettings.gameTextScale = value), - min: 0.5, - max: 2.0, - divisions: 15, - ), - ), - Text( - '${(globalSettings.gameTextScale * baseFontSize).toStringAsFixed(0)}pt (${(globalSettings.gameTextScale*100).toStringAsFixed(0)}%)'), - ], - ), - ], - ), - ), - ], - ), - ), + body: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (pageController.hasClients) _buildSidebar(), + _buildSettingsPages(), + ], ), floatingActionButton: FloatingActionButton( onPressed: () => widget.onSave(settings, globalSettings), @@ -150,5 +67,166 @@ class _SettingsPageState extends State with GameStoreStateMixin { ), ); } + + SizedBox _buildSettingsPages() { + return SizedBox( + width: 700, + child: PageView( + controller: pageController, + children: [ + if (settings != null) _buildProfileSettings(), + _buildGlobalSettings(), + ], + ), + ); + } + + ListView _buildGlobalSettings() { + final baseFontSize = Theme.of(context).textTheme.bodyMedium!.fontSize!; + return ListView( + children: [ + CheckboxListTile.adaptive( + value: globalSettings.keepAwake, + onChanged: (value) => + setState(() => globalSettings.keepAwake = value!), + title: const Text('Keep Screen Awake'), + subtitle: const Text( + 'Enabling this will make sure the screen doesn\'t turn off while a session is running.', + ), + ), + ListTile( + title: const Text('UI Font Size'), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Change the font size of the entire UI.'), + Row( + children: [ + Expanded( + child: Slider( + value: globalSettings.uiTextScale, + onChanged: (value) => + setState(() => globalSettings.uiTextScale = value), + min: 0.5, + max: 2.0, + divisions: 15, + ), + ), + Text( + _formatFontSize(baseFontSize, globalSettings.uiTextScale), + ), + ], + ), + ], + ), + ), + ListTile( + title: const Text('Game Output Font Size'), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Change the font size of the game output text.'), + Row( + children: [ + Expanded( + child: Slider( + value: globalSettings.gameTextScale, + onChanged: (value) => + setState(() => globalSettings.gameTextScale = value), + min: 0.5, + max: 2.0, + divisions: 15, + ), + ), + Text( + _formatFontSize(baseFontSize, globalSettings.gameTextScale), + ), + ], + ), + ], + ), + ), + ], + ); + } + + String _formatFontSize(double baseFontSize, double value) { + final ptSize = (value * baseFontSize).toStringAsFixed(0); + final percent = (value * 100).toStringAsFixed(0); + return '${ptSize}pt ($percent%)'; + } + + ListView _buildProfileSettings() { + return ListView( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: TextFormField( + initialValue: settings!.commandSeparator, + onChanged: (value) => settings!.commandSeparator = value, + maxLength: 1, + decoration: const InputDecoration( + labelText: 'Command Separator', + helperText: + 'The character that separates commands.\nTo send it literally, use it twice.', + ), + ), + ), + const SizedBox(height: 16), + CheckboxListTile.adaptive( + value: settings!.echoCommands, + onChanged: (value) => setState(() => settings!.echoCommands = value!), + title: const Text('Echo Commands'), + subtitle: const Text( + 'Whether to echo commands to the screen as they are sent.', + ), + ), + CheckboxListTile.adaptive( + value: settings!.showTimestamps, + onChanged: (value) => + setState(() => settings!.showTimestamps = value!), + title: const Text('Show Timestamps'), + subtitle: const Text( + 'Whether to show timestamps on messages received from the server.', + ), + ), + const SizedBox(height: 16), + ], + ); + } + + SizedBox _buildSidebar() { + final drawerTitleStyle = Theme.of(context).textTheme.titleMedium; + final globalIndex = settings != null ? 1 : 0; + + return SizedBox( + width: 300, + child: ListView( + children: [ + if (settings != null) + ListTile( + title: Text('Profile Settings', style: drawerTitleStyle), + selected: pageController.page?.round() == 0, + onTap: () => pageController.animateToPage( + 0, + duration: const Duration(milliseconds: 200), + curve: Curves.easeInOut, + ), + ), + ListTile( + title: Text('Global Settings', style: drawerTitleStyle), + selected: pageController.page?.round() == globalIndex, + onTap: () { + pageController.animateToPage( + globalIndex, + duration: const Duration(milliseconds: 200), + curve: Curves.easeInOut, + ); + }, + ), + ], + ), + ); + } }