diff --git a/lib/features/settings/presentation/settings_view.dart b/lib/features/settings/presentation/settings_view.dart index da8b4af..f437aba 100644 --- a/lib/features/settings/presentation/settings_view.dart +++ b/lib/features/settings/presentation/settings_view.dart @@ -520,11 +520,12 @@ class _ShareDiagnosticsDialog extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: events .map( - (ShareIntakeDebugEvent event) => Padding( - padding: const EdgeInsets.only(bottom: 10), - child: SelectableText( - '${_debugTimeLabel(event.at)} ${event.stage}\n${event.message}', - ), + ( + ShareIntakeDebugEvent event, + ) => _ShareDiagnosticsEventTile( + event: event, + label: + '${_debugTimeLabel(event.at)} ${event.stage}\n${event.message}', ), ) .toList(growable: false), @@ -545,6 +546,51 @@ class _ShareDiagnosticsDialog extends StatelessWidget { } } +class _ShareDiagnosticsEventTile extends StatelessWidget { + const _ShareDiagnosticsEventTile({required this.event, required this.label}); + + final ShareIntakeDebugEvent event; + final String label; + + @override + Widget build(BuildContext context) { + final String? details = event.details; + if (details == null || details.isEmpty) { + return Padding( + padding: const EdgeInsets.only(bottom: 10), + child: SelectableText(label), + ); + } + + return Padding( + padding: const EdgeInsets.only(bottom: 10), + child: ExpansionTile( + tilePadding: EdgeInsets.zero, + childrenPadding: const EdgeInsets.only(top: 6, bottom: 8), + title: SelectableText(label), + subtitle: Text( + 'Tap to show raw shared text (${details.length} chars).', + ), + children: [ + DecoratedBox( + decoration: BoxDecoration( + border: Border.all(color: Theme.of(context).dividerColor), + borderRadius: BorderRadius.circular(6), + ), + child: Padding( + padding: const EdgeInsets.all(10), + child: Align( + alignment: Alignment.centerLeft, + child: SelectableText(details), + ), + ), + ), + ], + ), + ); + } +} + String _debugTimeLabel(DateTime value) { return '${value.hour.toString().padLeft(2, '0')}:${value.minute.toString().padLeft(2, '0')}:${value.second.toString().padLeft(2, '0')}'; } diff --git a/lib/features/share_intake/application/share_intake_debug_log.dart b/lib/features/share_intake/application/share_intake_debug_log.dart index a7e5368..e65a39b 100644 --- a/lib/features/share_intake/application/share_intake_debug_log.dart +++ b/lib/features/share_intake/application/share_intake_debug_log.dart @@ -4,12 +4,14 @@ import 'package:shared_preferences/shared_preferences.dart'; const String _shareIntakeDebugLogKey = 'share_intake_debug_log_v1'; const int _maxShareIntakeDebugEvents = 40; +const int _maxShareIntakeDebugDetailChars = 6000; class ShareIntakeDebugEvent { const ShareIntakeDebugEvent({ required this.at, required this.stage, required this.message, + this.details, }); factory ShareIntakeDebugEvent.fromJson(Map json) { @@ -19,18 +21,21 @@ class ShareIntakeDebugEvent { ).toLocal(), stage: json['stage'] as String? ?? 'unknown', message: json['message'] as String? ?? '', + details: json['details'] as String?, ); } final DateTime at; final String stage; final String message; + final String? details; Map toJson() { return { 'at': at.toUtc().toIso8601String(), 'stage': stage, 'message': message, + if (details != null) 'details': details, }; } } @@ -55,11 +60,16 @@ class ShareIntakeDebugLog { } } - Future record(String stage, String message) async { + Future record(String stage, String message, {String? details}) async { final SharedPreferences prefs = await SharedPreferences.getInstance(); final List current = await read(); final List next = [ - ShareIntakeDebugEvent(at: DateTime.now(), stage: stage, message: message), + ShareIntakeDebugEvent( + at: DateTime.now(), + stage: stage, + message: message, + details: _compactDetails(details), + ), ...current, ].take(_maxShareIntakeDebugEvents).toList(growable: false); await prefs.setString( @@ -77,3 +87,14 @@ class ShareIntakeDebugLog { await prefs.remove(_shareIntakeDebugLogKey); } } + +String? _compactDetails(String? value) { + final String? trimmed = value?.trim(); + if (trimmed == null || trimmed.isEmpty) { + return null; + } + if (trimmed.length <= _maxShareIntakeDebugDetailChars) { + return trimmed; + } + return '${trimmed.substring(0, _maxShareIntakeDebugDetailChars)}\n[truncated]'; +} diff --git a/lib/features/share_intake/presentation/incoming_share_listener.dart b/lib/features/share_intake/presentation/incoming_share_listener.dart index e4a30ff..aa3ce3d 100644 --- a/lib/features/share_intake/presentation/incoming_share_listener.dart +++ b/lib/features/share_intake/presentation/incoming_share_listener.dart @@ -159,6 +159,12 @@ class _IncomingShareListenerState extends ConsumerState { return; } + await _debugLog.record( + 'raw_share_text', + 'Captured raw share text (${trimmed.length} chars).', + details: trimmed, + ); + final SharedPayload? payload = buildSharedPayloadFromRawText( rawText: trimmed, platform: defaultTargetPlatform.name, diff --git a/test/features/share_intake/share_intake_debug_log_test.dart b/test/features/share_intake/share_intake_debug_log_test.dart new file mode 100644 index 0000000..e86f818 --- /dev/null +++ b/test/features/share_intake/share_intake_debug_log_test.dart @@ -0,0 +1,40 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:relationship_saver/features/share_intake/application/share_intake_debug_log.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +void main() { + setUp(() { + SharedPreferences.setMockInitialValues({}); + }); + + test('records optional share details for diagnostics', () async { + const ShareIntakeDebugLog log = ShareIntakeDebugLog(); + + await log.record( + 'raw_share_text', + 'Captured raw share text (18 chars).', + details: 'Alex: likes cycling', + ); + + final List events = await log.read(); + + expect(events, hasLength(1)); + expect(events.single.stage, 'raw_share_text'); + expect(events.single.details, 'Alex: likes cycling'); + }); + + test('trims very large details before storing', () async { + const ShareIntakeDebugLog log = ShareIntakeDebugLog(); + + await log.record( + 'raw_share_text', + 'large', + details: List.filled(7000, 'x').join(), + ); + + final List events = await log.read(); + + expect(events.single.details, endsWith('[truncated]')); + expect(events.single.details!.length, lessThan(7000)); + }); +}