mirror of
https://github.com/chenasraf/mudblock.git
synced 2026-05-17 17:48:05 +00:00
feat: settings updates, auth post send
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -21,6 +21,7 @@ final profilePresets = [
|
||||
host: 'aardmud.org',
|
||||
port: 23,
|
||||
authMethod: AuthMethod.diku,
|
||||
authPostSend: true,
|
||||
),
|
||||
MUDProfile(
|
||||
id: 'batmud',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
28
lib/core/theme.dart
Normal 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;
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user