diff --git a/assets/i18n/en.json b/assets/i18n/en.json index 5debbde..2b8a5ce 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -163,7 +163,9 @@ "developer": { "title": "Developer Logs", "command": "Command", - "copied": "Selection copied" + "copied": "Selection copied", + "cleared": "Logs cleared", + "are_you_sure_clear": "Are you sure you want to clear the logs?" }, "log": { "error": "Error", diff --git a/lib/main.dart b/lib/main.dart index 393d52a..f73c6ba 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:io'; +import 'package:ansicolor/ansicolor.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -23,6 +24,9 @@ void main() async { debugPrint('VeilidChat PID: $pid'); } + // Ansi colors + ansiColorDisabled = false; + // Logs initLoggy(); diff --git a/lib/pages/developer.dart b/lib/pages/developer.dart index a021cea..da78c9c 100644 --- a/lib/pages/developer.dart +++ b/lib/pages/developer.dart @@ -1,11 +1,16 @@ +import 'package:ansicolor/ansicolor.dart'; import 'package:awesome_extensions/awesome_extensions.dart'; +import 'package:cool_dropdown/cool_dropdown.dart'; +import 'package:cool_dropdown/models/cool_dropdown_item.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_translate/flutter_translate.dart'; import 'package:go_router/go_router.dart'; import 'package:loggy/loggy.dart'; +import 'package:quickalert/quickalert.dart'; import 'package:xterm/xterm.dart'; import '../tools/tools.dart'; @@ -28,60 +33,63 @@ class DeveloperPage extends ConsumerStatefulWidget { } class DeveloperPageState extends ConsumerState { - final terminalController = TerminalController(); - var logLevelDropDown = log.level.logLevel; - final TextEditingController _debugCommandController = TextEditingController(); + final _terminalController = TerminalController(); + final _debugCommandController = TextEditingController(); + final _logLevelController = DropdownController(duration: 250.ms); + final List> _logLevelDropdownItems = []; + var _logLevelDropDown = log.level.logLevel; + var _showEllet = false; @override void initState() { - // _scrollController = ScrollController( - // onAttach: _handlePositionAttach, - // onDetach: _handlePositionDetach, - // ); super.initState(); - terminalController.addListener(() { + _terminalController.addListener(() { setState(() {}); }); + + for (var i = 0; i < logLevels.length; i++) { + _logLevelDropdownItems.add(CoolDropdownItem( + label: logLevelName(logLevels[i]), + icon: Text(logLevelEmoji(logLevels[i])), + value: logLevels[i])); + } } - // void _handleScrollChange() { - // if (_isScrolling != _scrollController.position.isScrollingNotifier.value) { - // _isScrolling = _scrollController.position.isScrollingNotifier.value; - // _wantsBottom = _scrollController.position.pixels == - // _scrollController.position.maxScrollExtent; - // } - // } - - // void _handlePositionAttach(ScrollPosition position) { - // // From here, add a listener to the given ScrollPosition. - // // Here the isScrollingNotifier will be used to inform when scrolling starts - // // and stops and change the AppBar's color in response. - // position.isScrollingNotifier.addListener(_handleScrollChange); - // } - - // void _handlePositionDetach(ScrollPosition position) { - // // From here, add a listener to the given ScrollPosition. - // // Here the isScrollingNotifier will be used to inform when scrolling starts - // // and stops and change the AppBar's color in response. - // position.isScrollingNotifier.removeListener(_handleScrollChange); - // } - - // void _scrollToBottom() { - // _scrollController.jumpTo(_scrollController.position.maxScrollExtent); - // _wantsBottom = true; - // } + void _debugOut(String out) { + final pen = AnsiPen()..cyan(bold: true); + final colorOut = pen(out); + debugPrint(colorOut); + globalDebugTerminal.write(colorOut.replaceAll('\n', '\r\n')); + } Future _sendDebugCommand(String debugCommand) async { - log.info('DEBUG >>>\n$debugCommand'); - final out = await Veilid.instance.debug(debugCommand); - log.info('<<< DEBUG\n$out'); + if (debugCommand == 'ellet') { + setState(() { + _showEllet = !_showEllet; + }); + return; + } + _debugOut('DEBUG >>>\n$debugCommand\n'); + try { + final out = await Veilid.instance.debug(debugCommand); + _debugOut('<<< DEBUG\n$out\n'); + } on Exception catch (e, st) { + _debugOut('<<< ERROR\n$e\n<<< STACK\n$st'); + } + } + + Future clear(BuildContext context) async { + globalDebugTerminal.buffer.clear(); + if (context.mounted) { + showInfoToast(context, translate('developer.cleared')); + } } Future copySelection(BuildContext context) async { - final selection = terminalController.selection; + final selection = _terminalController.selection; if (selection != null) { final text = globalDebugTerminal.buffer.getText(selection); - terminalController.clearSelection(); + _terminalController.clearSelection(); await Clipboard.setData(ClipboardData(text: text)); if (context.mounted) { showInfoToast(context, translate('developer.copied')); @@ -92,7 +100,7 @@ class DeveloperPageState extends ConsumerState { @override Widget build(BuildContext context) { final theme = Theme.of(context); - //final textTheme = theme.textTheme; + final textTheme = theme.textTheme; final scale = theme.extension()!; // WidgetsBinding.instance.addPostFrameCallback((_) { @@ -112,49 +120,99 @@ class DeveloperPageState extends ConsumerState { icon: const Icon(Icons.copy), color: scale.primaryScale.text, disabledColor: scale.grayScale.subtleText, - onPressed: terminalController.selection == null + onPressed: _terminalController.selection == null ? null : () async { await copySelection(context); }), - DropdownMenu( - initialSelection: logLevelDropDown, - onSelected: (value) { - if (value != null) { - setState(() { - logLevelDropDown = value; - //log. = value; - setVeilidLogLevel(value); - }); - } - }, - dropdownMenuEntries: [ - DropdownMenuEntry( - value: LogLevel.error, label: translate('log.error')), - DropdownMenuEntry( - value: LogLevel.warning, label: translate('log.warning')), - DropdownMenuEntry( - value: LogLevel.info, label: translate('log.info')), - DropdownMenuEntry( - value: LogLevel.debug, label: translate('log.debug')), - DropdownMenuEntry( - value: traceLevel, label: translate('log.trace')), - ]) + IconButton( + icon: const Icon(Icons.clear_all), + color: scale.primaryScale.text, + disabledColor: scale.grayScale.subtleText, + onPressed: () async { + await QuickAlert.show( + context: context, + type: QuickAlertType.confirm, + title: translate('developer.are_you_sure_clear'), + textColor: scale.primaryScale.text, + confirmBtnColor: scale.primaryScale.elementBackground, + backgroundColor: scale.primaryScale.subtleBackground, + headerBackgroundColor: scale.primaryScale.background, + confirmBtnText: translate('button.ok'), + cancelBtnText: translate('button.cancel'), + onConfirmBtnTap: () async { + Navigator.pop(context); + if (context.mounted) { + await clear(context); + } + }); + }), + CoolDropdown( + controller: _logLevelController, + defaultItem: _logLevelDropdownItems + .singleWhere((x) => x.value == _logLevelDropDown), + onChange: (value) { + setState(() { + _logLevelDropDown = value; + Loggy('').level = getLogOptions(value); + setVeilidLogLevel(value); + _logLevelController.close(); + }); + }, + resultOptions: ResultOptions( + width: 64, + height: 40, + render: ResultRender.icon, + textStyle: textTheme.labelMedium! + .copyWith(color: scale.primaryScale.text), + padding: const EdgeInsets.fromLTRB(8, 4, 8, 4), + openBoxDecoration: BoxDecoration( + color: scale.primaryScale.activeElementBackground), + boxDecoration: + BoxDecoration(color: scale.primaryScale.elementBackground), + ), + dropdownOptions: DropdownOptions( + width: 160, + align: DropdownAlign.right, + duration: 150.ms, + color: scale.primaryScale.elementBackground, + borderSide: BorderSide(color: scale.primaryScale.border), + borderRadius: BorderRadius.circular(8), + padding: const EdgeInsets.fromLTRB(8, 4, 8, 4), + ), + dropdownTriangleOptions: const DropdownTriangleOptions( + align: DropdownTriangleAlign.right), + dropdownItemOptions: DropdownItemOptions( + selectedTextStyle: textTheme.labelMedium! + .copyWith(color: scale.primaryScale.text), + textStyle: textTheme.labelMedium! + .copyWith(color: scale.primaryScale.text), + selectedBoxDecoration: BoxDecoration( + color: scale.primaryScale.activeElementBackground), + mainAxisAlignment: MainAxisAlignment.spaceBetween, + padding: const EdgeInsets.fromLTRB(8, 4, 8, 4), + selectedPadding: const EdgeInsets.fromLTRB(8, 4, 8, 4)), + dropdownList: _logLevelDropdownItems, + ) ], - title: Text(translate('developer.title')), + title: Text(translate('developer.title'), + style: + textTheme.bodyLarge!.copyWith(fontWeight: FontWeight.bold)), centerTitle: true, ), - body: Column(children: [ - TerminalView( - globalDebugTerminal, - textStyle: kDefaultTerminalStyle, - controller: terminalController, - //autofocus: true, - //backgroundOpacity: 0.9, - onSecondaryTapDown: (details, offset) async { + body: SafeArea( + child: Column(children: [ + Stack(alignment: AlignmentDirectional.center, children: [ + Image.asset('assets/images/ellet.png'), + TerminalView(globalDebugTerminal, + textStyle: kDefaultTerminalStyle, + controller: _terminalController, + //autofocus: true, + backgroundOpacity: _showEllet ? 0.75 : 1.0, + onSecondaryTapDown: (details, offset) async { await copySelection(context); - }, - ).expanded(), + }) + ]).expanded(), TextField( controller: _debugCommandController, decoration: InputDecoration( @@ -167,24 +225,32 @@ class DeveloperPageState extends ConsumerState { hintText: translate('developer.command'), suffixIcon: IconButton( icon: const Icon(Icons.send), - onPressed: () async { - final debugCommand = _debugCommandController.text; - _debugCommandController.clear(); - await _sendDebugCommand(debugCommand); - }, + onPressed: _debugCommandController.text.isEmpty + ? null + : () async { + final debugCommand = _debugCommandController.text; + _debugCommandController.clear(); + await _sendDebugCommand(debugCommand); + }, )), + onChanged: (_) { + setState(() => {}); + }, onSubmitted: (debugCommand) async { _debugCommandController.clear(); await _sendDebugCommand(debugCommand); }, ).paddingAll(4) - ])); + ]))); } @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); - properties.add(DiagnosticsProperty( - 'terminalController', terminalController)); + properties + ..add(DiagnosticsProperty( + 'terminalController', _terminalController)) + ..add( + DiagnosticsProperty('logLevelDropDown', _logLevelDropDown)); } } diff --git a/lib/pages/new_account.dart b/lib/pages/new_account.dart index 6a80bb4..3ef1b6d 100644 --- a/lib/pages/new_account.dart +++ b/lib/pages/new_account.dart @@ -153,7 +153,6 @@ class NewAccountPageState extends ConsumerState { body: _newAccountForm( context, onSubmit: (formKey) async { - debugPrint(_formKey.currentState?.value.toString()); FocusScope.of(context).unfocus(); try { await createAccount(); diff --git a/lib/router/router_notifier.dart b/lib/router/router_notifier.dart index 9da3d4c..a4f7bb6 100644 --- a/lib/router/router_notifier.dart +++ b/lib/router/router_notifier.dart @@ -92,6 +92,8 @@ class RouterNotifier extends _$RouterNotifier implements Listenable { case '/home/settings': case '/new_account/settings': return null; + case '/developer': + return null; default: return hasAnyAccount ? null : '/new_account'; } diff --git a/lib/tools/loggy.dart b/lib/tools/loggy.dart index 5ecb3f6..fc07d3f 100644 --- a/lib/tools/loggy.dart +++ b/lib/tools/loggy.dart @@ -2,6 +2,7 @@ import 'dart:io' show Platform; import 'package:ansicolor/ansicolor.dart'; import 'package:flutter/foundation.dart'; +import 'package:flutter_translate/flutter_translate.dart'; import 'package:intl/intl.dart'; import 'package:loggy/loggy.dart'; @@ -54,13 +55,37 @@ final DateFormat _dateFormatter = DateFormat('HH:mm:ss.SSS'); extension PrettyPrintLogRecord on LogRecord { String pretty() { final tm = _dateFormatter.format(time.toLocal()); - final lev = logEmoji(level); + final lev = logLevelEmoji(level); final lstr = wrapWithLogColor(level, tm); return '$lstr $lev $message'; } } -String logEmoji(LogLevel logLevel) { +List logLevels = [ + LogLevel.error, + LogLevel.warning, + LogLevel.info, + LogLevel.debug, + traceLevel, +]; + +String logLevelName(LogLevel logLevel) { + switch (logLevel) { + case traceLevel: + return translate('log.trace'); + case LogLevel.debug: + return translate('log.debug'); + case LogLevel.info: + return translate('log.info'); + case LogLevel.warning: + return translate('log.warning'); + case LogLevel.error: + return translate('log.error'); + } + return '???'; +} + +String logLevelEmoji(LogLevel logLevel) { switch (logLevel) { case traceLevel: return '👾'; @@ -69,7 +94,7 @@ String logEmoji(LogLevel logLevel) { case LogLevel.info: return '💡'; case LogLevel.warning: - return '⚠️'; + return '🍋'; case LogLevel.error: return '🛑'; } diff --git a/pubspec.lock b/pubspec.lock index fa8be95..5fefb4e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -337,6 +337,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.1" + cool_dropdown: + dependency: "direct main" + description: + name: cool_dropdown + sha256: "24400f57740b4779407586121e014bef241699ad2a52c506a7e1e7616cb68653" + url: "https://pub.dev" + source: hosted + version: "2.1.0" cross_file: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index f560561..becf91b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -19,6 +19,7 @@ dependencies: charcode: ^1.3.1 circular_profile_avatar: ^2.0.5 circular_reveal_animation: ^2.0.1 + cool_dropdown: ^2.1.0 cupertino_icons: ^1.0.2 equatable: ^2.0.5 fast_immutable_collections: ^9.1.5 @@ -119,6 +120,7 @@ flutter: - assets/images/icon.svg - assets/images/title.svg - assets/images/vlogo.svg + - assets/images/ellet.png # Fonts fonts: - family: Source Code Pro