From eb4dcfe5d33edf729d83c8777dea6cc93e3f7204 Mon Sep 17 00:00:00 2001 From: Udhay-Adithya Date: Sun, 14 Sep 2025 01:26:37 +0530 Subject: [PATCH] feat: add timestamp toggle and request name to terminal log tiles --- lib/screens/terminal/terminal_page.dart | 59 +++++++++++++++++- lib/widgets/terminal_tiles.dart | 79 +++++++++++++++++++++---- 2 files changed, 125 insertions(+), 13 deletions(-) diff --git a/lib/screens/terminal/terminal_page.dart b/lib/screens/terminal/terminal_page.dart index 838fd4cc..0a2f2dd1 100644 --- a/lib/screens/terminal/terminal_page.dart +++ b/lib/screens/terminal/terminal_page.dart @@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../models/terminal/models.dart'; import '../../consts.dart'; import '../../providers/terminal_providers.dart'; +import '../../providers/collection_providers.dart'; import '../../widgets/button_copy.dart'; import '../../widgets/field_search.dart'; import '../../widgets/terminal_tiles.dart'; @@ -18,6 +19,7 @@ class TerminalPage extends ConsumerStatefulWidget { class _TerminalPageState extends ConsumerState { final TextEditingController _searchCtrl = TextEditingController(); + bool _showTimestamps = false; // user toggle // Initially all levels will be selected final Set _selectedLevels = { @@ -36,6 +38,8 @@ class _TerminalPageState extends ConsumerState { @override Widget build(BuildContext context) { final state = ref.watch(terminalStateProvider); + final collection = ref.watch(collectionStateNotifierProvider); + final selectedId = ref.watch(selectedIdStateProvider); final allEntries = state.entries; final filtered = _applyFilters(allEntries); @@ -58,13 +62,46 @@ class _TerminalPageState extends ConsumerState { separatorBuilder: (_, __) => const Divider(height: 1), itemBuilder: (ctx, i) { final e = filtered[filtered.length - 1 - i]; + String requestName = ''; + if (e.source == TerminalSource.js) { + if (selectedId != null) { + final model = collection?[selectedId]; + if (model != null) { + requestName = + model.name.isNotEmpty ? model.name : model.id; + } else { + requestName = selectedId; + } + } + } else if (e.requestId != null) { + final model = collection?[e.requestId]; + if (model != null) { + requestName = + model.name.isNotEmpty ? model.name : model.id; + } else { + requestName = e.requestId!; + } + } switch (e.source) { case TerminalSource.js: - return JsLogTile(entry: e); + return JsLogTile( + entry: e, + showTimestamp: _showTimestamps, + requestName: + requestName.isNotEmpty ? requestName : null, + ); case TerminalSource.network: - return NetworkLogTile(entry: e); + return NetworkLogTile( + entry: e, + showTimestamp: _showTimestamps, + requestName: + requestName.isNotEmpty ? requestName : null, + ); case TerminalSource.system: - return SystemLogTile(entry: e); + return SystemLogTile( + entry: e, + showTimestamp: _showTimestamps, + ); } }, ), @@ -96,6 +133,22 @@ class _TerminalPageState extends ConsumerState { ..addAll(set); }), ), + const SizedBox(width: 4), + Tooltip( + message: 'Show timestamps', + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Checkbox( + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + value: _showTimestamps, + onChanged: (v) => + setState(() => _showTimestamps = v ?? false), + ), + const Text('Timestamp', style: TextStyle(fontSize: 12)), + ], + ), + ), const Spacer(), // Clear button diff --git a/lib/widgets/terminal_tiles.dart b/lib/widgets/terminal_tiles.dart index 35f73e62..e70094cf 100644 --- a/lib/widgets/terminal_tiles.dart +++ b/lib/widgets/terminal_tiles.dart @@ -4,8 +4,10 @@ import '../models/terminal/models.dart'; import 'expandable_section.dart'; class SystemLogTile extends StatelessWidget { - const SystemLogTile({super.key, required this.entry}); + const SystemLogTile( + {super.key, required this.entry, this.showTimestamp = false}); final TerminalEntry entry; + final bool showTimestamp; @override Widget build(BuildContext context) { assert(entry.system != null, 'System tile requires SystemLogData'); @@ -35,10 +37,17 @@ class SystemLogTile extends StatelessWidget { child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Padding( - padding: const EdgeInsets.only(top: 2, right: 8), - child: Icon(icon, size: 18, color: iconColor), - ), + if (showTimestamp) ...[ + Padding( + padding: const EdgeInsets.only(top: 2, right: 8), + child: Text(_formatTs(entry.ts), style: subStyle), + ), + ] else ...[ + Padding( + padding: const EdgeInsets.only(top: 2, right: 8), + child: Icon(icon, size: 18, color: iconColor), + ), + ], Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -58,8 +67,14 @@ class SystemLogTile extends StatelessWidget { } class JsLogTile extends StatelessWidget { - const JsLogTile({super.key, required this.entry}); + const JsLogTile( + {super.key, + required this.entry, + this.showTimestamp = false, + this.requestName}); final TerminalEntry entry; + final bool showTimestamp; + final String? requestName; @override Widget build(BuildContext context) { assert(entry.js != null, 'JS tile requires JsLogData'); @@ -83,13 +98,29 @@ class JsLogTile extends StatelessWidget { case TerminalLevel.debug: break; } + final bodyParts = []; + if (requestName != null && requestName!.isNotEmpty) { + bodyParts.add('[$requestName]'); + } + // Add JS level/context prefix to disambiguate + if (j.context != null && j.context!.isNotEmpty) { + bodyParts.add('(${j.context})'); + } + bodyParts.addAll(j.args); + final bodyText = bodyParts.join(' '); return Container( color: bg, padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (icon != null) ...[ + if (showTimestamp) ...[ + Padding( + padding: const EdgeInsets.only(top: 2, right: 8), + child: Text(_formatTs(entry.ts), + style: Theme.of(context).textTheme.bodySmall), + ), + ] else if (icon != null) ...[ Padding( padding: const EdgeInsets.only(top: 2, right: 8), child: Icon(icon, size: 18, color: iconColor), @@ -97,7 +128,7 @@ class JsLogTile extends StatelessWidget { ], Expanded( child: SelectableText( - j.args.join(' '), + bodyText, style: Theme.of(context).textTheme.bodyMedium, ), ), @@ -108,8 +139,14 @@ class JsLogTile extends StatelessWidget { } class NetworkLogTile extends StatefulWidget { - const NetworkLogTile({super.key, required this.entry}); + const NetworkLogTile( + {super.key, + required this.entry, + this.showTimestamp = false, + this.requestName}); final TerminalEntry entry; + final bool showTimestamp; + final String? requestName; @override State createState() => _NetworkLogTileState(); } @@ -123,6 +160,11 @@ class _NetworkLogTileState extends State { final status = n.responseStatus != null ? '${n.responseStatus}' : null; final duration = n.duration != null ? '${n.duration!.inMilliseconds} ms' : null; + final title = [ + if (widget.requestName != null && widget.requestName!.isNotEmpty) + '[${widget.requestName}]', + methodUrl, + ].join(' '); return Column( children: [ InkWell( @@ -131,9 +173,18 @@ class _NetworkLogTileState extends State { padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), child: Row( children: [ + if (widget.showTimestamp) ...[ + Padding( + padding: const EdgeInsets.only(right: 8), + child: Text( + _formatTs(widget.entry.ts), + style: Theme.of(context).textTheme.bodySmall, + ), + ), + ], Expanded( child: Text( - methodUrl, + title, maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.bodyMedium, @@ -220,3 +271,11 @@ class NetworkDetails extends StatelessWidget { return SelectableText(lines); } } + +String _formatTs(DateTime ts) { + // Show only time (HH:mm:ss) for compactness + final h = ts.hour.toString().padLeft(2, '0'); + final m = ts.minute.toString().padLeft(2, '0'); + final s = ts.second.toString().padLeft(2, '0'); + return '$h:$m:$s'; +}