import 'package:clipboard/clipboard.dart'; import 'package:finamp/services/censored_log.dart'; import 'package:finamp/services/contains_login.dart'; import 'package:flutter/material.dart'; import 'package:finamp/l10n/app_localizations.dart'; import 'package:logging/logging.dart'; import '../error_snackbar.dart'; class LogTile extends StatefulWidget { const LogTile({Key? key, required this.logRecord}) : super(key: key); final LogRecord logRecord; @override State createState() => _LogTileState(); } class _LogTileState extends State { final _controller = ExpansionTileController(); /// Whether the user has confirmed. This is used to stop onExpansionChanged /// from infinitely asking the user to confirm. bool hasConfirmed = false; @override Widget build(BuildContext context) { return GestureDetector( onLongPress: () async { try { await FlutterClipboard.copy(widget.logRecord.censoredMessage); } catch (e) { errorSnackbar(e, context); } }, child: Card( color: _logColor(widget.logRecord.level, context), child: ExpansionTile( controller: _controller, key: PageStorageKey(widget.logRecord.time), leading: _LogIcon(level: widget.logRecord.level), title: RichText( maxLines: 2, overflow: TextOverflow.ellipsis, text: TextSpan( text: "[${widget.logRecord.loggerName}]\n${widget.logRecord.time}", style: Theme.of(context).textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.bold, ), ), ), subtitle: RichText( maxLines: 2, overflow: TextOverflow.ellipsis, text: TextSpan( text: widget.logRecord.loginCensoredMessage, style: Theme.of(context).textTheme.bodyMedium, ), ), expandedAlignment: Alignment.centerLeft, expandedCrossAxisAlignment: CrossAxisAlignment.start, textColor: _logTextColor(widget.logRecord.level, context), collapsedTextColor: _logTextColor(widget.logRecord.level, context), iconColor: _logTextColor(widget.logRecord.level, context), collapsedIconColor: _logTextColor(widget.logRecord.level, context), // Remove the border when expanded shape: const Border(), childrenPadding: const EdgeInsets.all(8.0), children: [ Text( AppLocalizations.of(context)!.message, style: Theme.of(context).textTheme.titleLarge, ), _LogMessageContent(widget.logRecord.message), const SizedBox(height: 16.0), if (widget.logRecord.stackTrace != null) Text( AppLocalizations.of(context)!.stackTrace, style: Theme.of(context).textTheme.titleLarge, ), if (widget.logRecord.stackTrace != null) _LogMessageContent(widget.logRecord.stackTrace.toString()), ], onExpansionChanged: (value) async { if (value && !hasConfirmed && widget.logRecord.containsLogin) { _controller.collapse(); final confirmed = await showDialog( context: context, barrierDismissible: false, builder: (context) { return AlertDialog( title: Text(AppLocalizations.of(context)!.confirm), content: Text( AppLocalizations.of(context)!.showUncensoredLogMessage), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(false), child: Text(MaterialLocalizations.of(context) .cancelButtonLabel), ), TextButton( onPressed: () => Navigator.of(context).pop(true), child: Text(AppLocalizations.of(context)!.confirm), ), ], ); }, ); hasConfirmed = confirmed!; if (confirmed) { _controller.expand(); } } else if (hasConfirmed) { hasConfirmed = false; } }, ), ), ); } Color _logColor(Level level, BuildContext context) { if (level == Level.WARNING) { return Theme.of(context).colorScheme.tertiaryContainer; } else if (level == Level.SEVERE) { return Theme.of(context).colorScheme.errorContainer; } else { return Theme.of(context).colorScheme.primaryContainer; } } Color _logTextColor(Level level, BuildContext context) { if (level == Level.WARNING) { return Theme.of(context).colorScheme.onTertiaryContainer; } else if (level == Level.SEVERE) { return Theme.of(context).colorScheme.onErrorContainer; } else { return Theme.of(context).colorScheme.onPrimaryContainer; } } } class _LogIcon extends StatelessWidget { const _LogIcon({Key? key, required this.level}) : super(key: key); final Level level; @override Widget build(BuildContext context) { if (level == Level.INFO) { return const Icon(Icons.info); } else if (level == Level.WARNING) { return const Icon(Icons.warning); } else if (level == Level.SEVERE) { return const Icon(Icons.error); } else { return const Icon(Icons.info); } } } class _LogMessageContent extends StatelessWidget { const _LogMessageContent(this.content, {Key? key}) : super(key: key); final String content; @override Widget build(BuildContext context) { return Text( content, style: const TextStyle( fontSize: 12.0, fontFamily: "monospace", ), ); } }