feat: settings updates, auth post send

This commit is contained in:
2024-04-11 00:09:40 +03:00
parent b765c71e17
commit 9f803ae8a0
9 changed files with 262 additions and 141 deletions

View File

@@ -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<String, dynamic> 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 {

View File

@@ -21,6 +21,7 @@ final profilePresets = [
host: 'aardmud.org',
port: 23,
authMethod: AuthMethod.diku,
authPostSend: true,
),
MUDProfile(
id: 'batmud',

View File

@@ -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;

View File

@@ -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();
}
}

28
lib/core/theme.dart Normal file
View File

@@ -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;
}

View File

@@ -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',

View File

@@ -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,

View File

@@ -109,6 +109,29 @@ class _ProfilePageState extends State<ProfilePage> {
'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<ProfilePage> {
},
),
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',
),
],
),
],
),

View File

@@ -22,127 +22,44 @@ class SettingsPage extends StatefulWidget {
class _SettingsPageState extends State<SettingsPage> 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<SettingsPage> 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,
);
},
),
],
),
);
}
}