import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:relationship_saver/core/config/app_config.dart'; import 'package:relationship_saver/core/config/app_theme.dart'; import 'package:relationship_saver/core/llm/llm_config.dart'; import 'package:relationship_saver/core/llm/llm_service.dart'; import 'package:relationship_saver/features/ai_digest/application/llm_digest_background_scheduler.dart'; import 'package:relationship_saver/features/ai_digest/application/llm_digest_orchestrator.dart'; import 'package:relationship_saver/features/ai_digest/data/llm_digest_config.dart'; import 'package:relationship_saver/features/ai_digest/presentation/ai_digest_review_view.dart'; import 'package:relationship_saver/features/auth/session_controller.dart'; import 'package:relationship_saver/features/local/local_models.dart'; import 'package:relationship_saver/features/reminders/scheduling/reminder_scheduler_provider.dart'; import 'package:relationship_saver/features/share_intake/application/share_intake_debug_log.dart'; import 'package:relationship_saver/features/share_intake/share_capture_flow.dart'; import 'package:relationship_saver/features/share_intake/share_capture_models.dart'; import 'package:relationship_saver/features/share_intake/share_inbox_view.dart'; import 'package:relationship_saver/features/shared/frosted_card.dart'; import 'package:relationship_saver/integrations/backend/models/backend_models.dart'; class SettingsView extends ConsumerWidget { const SettingsView({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final AuthSession? session = ref .watch(sessionControllerProvider) .asData ?.value; final LlmDigestConfigState digestConfig = ref.watch( llmDigestConfigProvider, ); return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { final bool compact = constraints.maxWidth < 760; return Padding( padding: EdgeInsets.fromLTRB( compact ? 16 : 28, compact ? 16 : 24, compact ? 16 : 28, compact ? 20 : 28, ), child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Settings', style: Theme.of(context).textTheme.headlineMedium, ), const SizedBox(height: 6), Text( 'Environment and integration configuration for this build.', style: Theme.of(context).textTheme.bodyLarge?.copyWith( color: AppTheme.textSecondary, ), ), const SizedBox(height: 16), FrostedCard( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _SettingRow( compact: compact, label: 'Signed in as', value: session?.user.email ?? session?.user.id ?? 'Not signed in', ), const SizedBox(height: 12), _SettingRow( compact: compact, label: 'Gateway mode', value: AppConfig.useFakeBackend ? 'Fake (offline-safe)' : 'REST', ), const SizedBox(height: 12), _SettingRow( compact: compact, label: 'Base URL', value: AppConfig.backendBaseUrl, ), const SizedBox(height: 12), _SettingRow( compact: compact, label: 'Realtime', value: 'Planned later (SSE/WebSocket)', ), const SizedBox(height: 12), _SettingRow( compact: compact, label: 'Local notifications', value: AppConfig.enableLocalNotifications ? 'Enabled' : 'Disabled', ), const SizedBox(height: 12), _SettingRow( compact: compact, label: 'LLM AI', value: _llmStatusLabel(ref.watch(llmConfigProvider)), ), const SizedBox(height: 12), _SettingRow( compact: compact, label: 'AI digest', value: digestConfig.enabled ? 'Weekly, ${_weekdayLabel(digestConfig.preferredWeekday)} ${digestConfig.preferredHour.toString().padLeft(2, '0')}:00' : 'Off', ), const SizedBox(height: 12), _SettingRow( compact: compact, label: 'AI network', value: '${digestConfig.requireWifi ? 'Wi-Fi' : 'Any network'} + ${digestConfig.requireCharging ? 'charging' : 'battery allowed'}', ), const SizedBox(height: 14), Wrap( spacing: 8, runSpacing: 8, children: [ OutlinedButton.icon( onPressed: () => _configureLlm(context, ref), icon: const Icon(Icons.smart_toy_rounded), label: Text( ref.watch(llmConfigProvider).isConfigured ? 'Configure AI' : 'Setup AI', ), ), OutlinedButton.icon( onPressed: () => _showLlmDebug(context), icon: const Icon(Icons.terminal_rounded), label: const Text('LLM Debug'), ), OutlinedButton.icon( onPressed: () => _toggleDigest(context, ref), icon: Icon( digestConfig.enabled ? Icons.event_busy_rounded : Icons.event_repeat_rounded, ), label: Text( digestConfig.enabled ? 'Disable Weekly AI Digest' : 'Enable Weekly AI Digest', ), ), OutlinedButton.icon( onPressed: ref.watch(llmConfigProvider).isConfigured ? () => _runPrivateDigest(context, ref) : null, icon: const Icon(Icons.auto_awesome_rounded), label: const Text('Run Private Digest Now'), ), OutlinedButton.icon( onPressed: () => Navigator.of(context).push( MaterialPageRoute( builder: (BuildContext context) => const AiDigestReviewView(), ), ), icon: const Icon(Icons.rate_review_rounded), label: const Text('Open AI Review'), ), OutlinedButton.icon( onPressed: session == null ? null : () => _signOut(context, ref), icon: const Icon(Icons.logout_rounded), label: const Text('Sign Out'), ), OutlinedButton.icon( onPressed: session == null ? null : () { ref .read( sessionControllerProvider.notifier, ) .refreshProfile(); }, icon: const Icon(Icons.refresh_rounded), label: const Text('Refresh Profile'), ), OutlinedButton.icon( onPressed: AppConfig.enableLocalNotifications ? () => _requestNotificationPermissions( context, ref, ) : null, icon: const Icon( Icons.notifications_active_rounded, ), label: const Text('Request Notification Access'), ), OutlinedButton.icon( onPressed: () => _simulateShareCapture(context, ref), icon: const Icon(Icons.message_rounded), label: const Text('Simulate Share Capture'), ), OutlinedButton.icon( onPressed: () => Navigator.of(context).push( MaterialPageRoute( builder: (BuildContext context) => const ShareInboxView(), ), ), icon: const Icon(Icons.inbox_rounded), label: const Text('Open Share Inbox'), ), OutlinedButton.icon( onPressed: () => _showShareDiagnostics(context), icon: const Icon(Icons.bug_report_rounded), label: const Text('Share Diagnostics'), ), ], ), ], ), ), const SizedBox(height: 16), FrostedCard( child: Text( 'Local-first privacy\n• Relationship details stay on this device in the current build.\n• Scheduled AI uses pseudonymous person tokens and a review inbox.\n• Names, aliases, sender names, URLs, and raw shared text are not sent in the digest payload.\n• iOS background digest runs are best-effort; manual runs are available here.\n\nRun options\n• Fake mode: flutter run --dart-define=USE_FAKE_BACKEND=true\n• REST mode: flutter run --dart-define=USE_FAKE_BACKEND=false --dart-define=BACKEND_BASE_URL=https://your-api', style: Theme.of(context).textTheme.bodyLarge, ), ), ], ), ), ); }, ); } String _llmStatusLabel(LlmConfigState state) { if (!state.isConfigured) { return 'Not configured'; } return '${state.provider.label} / ${state.model}'; } String _weekdayLabel(int weekday) { return switch (weekday) { DateTime.monday => 'Mon', DateTime.tuesday => 'Tue', DateTime.wednesday => 'Wed', DateTime.thursday => 'Thu', DateTime.friday => 'Fri', DateTime.saturday => 'Sat', DateTime.sunday => 'Sun', _ => 'Sun', }; } Future _configureLlm(BuildContext context, WidgetRef ref) async { final LlmConfigState current = ref.read(llmConfigProvider); final _LlmConfigDraft? draft = await showDialog<_LlmConfigDraft>( context: context, builder: (BuildContext context) => _LlmConfigDialog( currentProvider: current.provider, currentBaseUrl: current.baseUrl, currentModel: current.model, hasApiKey: current.apiKeyConfigured, ), ); if (draft == null || !context.mounted) { return; } await ref .read(llmConfigProvider.notifier) .saveConfiguration( provider: draft.provider, baseUrl: draft.baseUrl, model: draft.model, apiKey: draft.apiKey, clearApiKey: draft.clearApiKey, ); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( draft.clearApiKey ? 'AI API key cleared.' : 'AI configuration saved.', ), ), ); } } Future _toggleDigest(BuildContext context, WidgetRef ref) async { final LlmDigestConfigState current = ref.read(llmDigestConfigProvider); final bool nextEnabled = !current.enabled; await ref.read(llmDigestConfigProvider.notifier).setEnabled(nextEnabled); await ref .read(llmDigestBackgroundSchedulerProvider) .configure(ref.read(llmDigestConfigProvider)); if (!context.mounted) { return; } ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( nextEnabled ? 'Weekly private AI digest enabled.' : 'Weekly private AI digest disabled.', ), ), ); } Future _showLlmDebug(BuildContext context) async { await showDialog( context: context, builder: (BuildContext context) => const _LlmDebugDialog(), ); } Future _runPrivateDigest(BuildContext context, WidgetRef ref) async { final ScaffoldMessengerState messenger = ScaffoldMessenger.of(context); messenger.showSnackBar( const SnackBar(content: Text('Running private AI digest...')), ); final LlmDigestRunResult result = await ref .read(llmDigestOrchestratorProvider) .runManualDigest(); if (!context.mounted) { return; } messenger.showSnackBar( SnackBar( content: Text( result.completed ? 'Private digest created ${result.createdDraftCount} suggestions.' : result.reason ?? 'Private digest did not complete.', ), ), ); } Future _signOut(BuildContext context, WidgetRef ref) async { await ref.read(sessionControllerProvider.notifier).signOut(); if (context.mounted) { ScaffoldMessenger.of( context, ).showSnackBar(const SnackBar(content: Text('Signed out.'))); } } Future _requestNotificationPermissions( BuildContext context, WidgetRef ref, ) async { final bool granted = await ref .read(reminderSchedulerProvider) .requestPermissions(); if (!context.mounted) { return; } ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( granted ? 'Notification access granted.' : 'Notification access not granted.', ), ), ); } Future _simulateShareCapture( BuildContext context, WidgetRef ref, ) async { final String platformName = Theme.of(context).platform.name; final _ShareSimulationDraft? draft = await showDialog<_ShareSimulationDraft>( context: context, builder: (BuildContext context) => const _ShareSimulationDialog(), ); if (draft == null) { return; } if (!context.mounted) { return; } final SharedPayload? payload = buildSharedPayloadFromRawText( rawText: draft.rawText, platform: platformName, sourceAppHint: draft.sourceAppHint, ); if (payload == null) { ScaffoldMessenger.of( context, ).showSnackBar(const SnackBar(content: Text('Shared payload is empty.'))); return; } final SharedMessageIngestResult? result = await startShareCaptureReview( context, ref: ref, payload: payload, ); if (!context.mounted || result == null) { return; } ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(shareCaptureResultMessage(result)), action: SnackBarAction( label: result.isQueuedForResolution ? 'Open Inbox' : 'Open Profile', onPressed: () => openShareCaptureDestination(context, ref: ref, result: result), ), ), ); } Future _showShareDiagnostics(BuildContext context) async { final ShareIntakeDebugLog debugLog = const ShareIntakeDebugLog(); final List events = await debugLog.read(); if (!context.mounted) { return; } await showDialog( context: context, builder: (BuildContext context) => _ShareDiagnosticsDialog( events: events, onClear: () async { await debugLog.clear(); if (context.mounted) { Navigator.of(context).pop(); } }, ), ); } } class _SettingRow extends StatelessWidget { const _SettingRow({ required this.label, required this.value, required this.compact, }); final String label; final String value; final bool compact; @override Widget build(BuildContext context) { if (compact) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, style: Theme.of( context, ).textTheme.titleMedium?.copyWith(color: AppTheme.textSecondary), ), const SizedBox(height: 4), Text(value, style: Theme.of(context).textTheme.bodyLarge), ], ); } return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: 140, child: Text( label, style: Theme.of( context, ).textTheme.titleMedium?.copyWith(color: AppTheme.textSecondary), ), ), Expanded( child: Text(value, style: Theme.of(context).textTheme.bodyLarge), ), ], ); } } class _ShareDiagnosticsDialog extends StatelessWidget { const _ShareDiagnosticsDialog({required this.events, required this.onClear}); final List events; final Future Function() onClear; @override Widget build(BuildContext context) { return AlertDialog( title: const Text('Share Diagnostics'), content: SizedBox( width: 520, child: events.isEmpty ? const Text( 'No Flutter share events recorded yet. If you tried sharing, the native share extension likely did not open the app or the plugin did not receive the URL callback.', ) : SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, 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}', ), ), ) .toList(growable: false), ), ), ), actions: [ TextButton( onPressed: events.isEmpty ? null : onClear, child: const Text('Clear'), ), FilledButton( onPressed: () => Navigator.of(context).pop(), child: const Text('Close'), ), ], ); } } String _debugTimeLabel(DateTime value) { return '${value.hour.toString().padLeft(2, '0')}:${value.minute.toString().padLeft(2, '0')}:${value.second.toString().padLeft(2, '0')}'; } class _LlmConfigDraft { const _LlmConfigDraft({ required this.provider, required this.baseUrl, required this.model, this.apiKey, this.clearApiKey = false, }); final LlmProvider provider; final String baseUrl; final String model; final String? apiKey; final bool clearApiKey; } class _LlmConfigDialog extends ConsumerStatefulWidget { const _LlmConfigDialog({ required this.currentProvider, required this.currentBaseUrl, required this.currentModel, required this.hasApiKey, }); final LlmProvider currentProvider; final String currentBaseUrl; final String currentModel; final bool hasApiKey; @override ConsumerState<_LlmConfigDialog> createState() => _LlmConfigDialogState(); } class _LlmConfigDialogState extends ConsumerState<_LlmConfigDialog> { late LlmProvider _selectedProvider; late final TextEditingController _apiKeyController; late final TextEditingController _baseUrlController; late final TextEditingController _modelController; List _models = const []; bool _loadingModels = false; String? _modelLoadMessage; @override void initState() { super.initState(); _selectedProvider = widget.currentProvider; _apiKeyController = TextEditingController(); _baseUrlController = TextEditingController(text: widget.currentBaseUrl); _modelController = TextEditingController(text: widget.currentModel); } @override void dispose() { _apiKeyController.dispose(); _baseUrlController.dispose(); _modelController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final bool apiKeyVisible = _selectedProvider.requiresApiKey || _selectedProvider == LlmProvider.openaiCompatible; return AlertDialog( title: const Text('LLM Connection'), content: SizedBox( width: 560, child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ DropdownButtonFormField( initialValue: _selectedProvider, decoration: const InputDecoration( labelText: 'Provider', border: OutlineInputBorder(), ), items: LlmProvider.values .map( (LlmProvider provider) => DropdownMenuItem( value: provider, child: Text(provider.label), ), ) .toList(growable: false), onChanged: (LlmProvider? value) { if (value == null) { return; } setState(() { _selectedProvider = value; _baseUrlController.text = value.defaultBaseUrl; _modelController.text = value.defaultModel; _models = const []; _modelLoadMessage = null; }); }, ), const SizedBox(height: 12), TextField( controller: _baseUrlController, decoration: InputDecoration( labelText: _selectedProvider.hasConfigurableBaseUrl ? 'Base URL' : 'Provider endpoint', helperText: _selectedProvider == LlmProvider.openaiCompatible ? 'Use the OpenAI-compatible API base, usually ending in /v1.' : null, border: const OutlineInputBorder(), ), ), if (apiKeyVisible) ...[ const SizedBox(height: 12), TextField( controller: _apiKeyController, obscureText: true, decoration: InputDecoration( labelText: _selectedProvider.requiresApiKey ? 'API key' : 'API key (optional)', hintText: widget.hasApiKey ? 'Leave empty to keep current key' : 'Paste provider key', border: const OutlineInputBorder(), ), ), ], const SizedBox(height: 12), Row( children: [ Expanded( child: _models.isEmpty ? TextField( controller: _modelController, decoration: const InputDecoration( labelText: 'Model', border: OutlineInputBorder(), ), ) : DropdownButtonFormField( initialValue: _modelController.text.isNotEmpty ? _modelController.text : _models.first.id, decoration: const InputDecoration( labelText: 'Model', border: OutlineInputBorder(), ), items: _models .map( (LlmModelInfo model) => DropdownMenuItem( value: model.id, child: Text(model.label), ), ) .toList(growable: false), onChanged: (String? value) { if (value != null) { _modelController.text = value; } }, ), ), const SizedBox(width: 8), IconButton.outlined( onPressed: _loadingModels ? null : _loadModels, icon: _loadingModels ? const SizedBox.square( dimension: 18, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.cloud_sync_rounded), tooltip: 'Load models from provider', ), ], ), if (_modelLoadMessage != null) ...[ const SizedBox(height: 8), Text( _modelLoadMessage!, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: AppTheme.textSecondary, ), ), ], const SizedBox(height: 12), Text( _selectedProvider == LlmProvider.ollama ? 'Ollama reads installed local models from /api/tags and sends chat requests to /api/chat.' : 'Model discovery uses this provider connection before saving the selected model.', style: Theme.of( context, ).textTheme.bodySmall?.copyWith(color: AppTheme.textSecondary), ), ], ), ), ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('Cancel'), ), if (widget.hasApiKey) TextButton( onPressed: () { Navigator.of(context).pop( _LlmConfigDraft( provider: _selectedProvider, baseUrl: _baseUrlController.text.trim(), model: _modelController.text.trim(), clearApiKey: true, ), ); }, child: const Text('Clear Key'), ), FilledButton( onPressed: () { Navigator.of(context).pop( _LlmConfigDraft( provider: _selectedProvider, baseUrl: _baseUrlController.text.trim(), model: _modelController.text.trim(), apiKey: _apiKeyController.text.trim().isEmpty ? null : _apiKeyController.text.trim(), ), ); }, child: const Text('Save'), ), ], ); } Future _loadModels() async { setState(() { _loadingModels = true; _modelLoadMessage = null; }); try { final List models = await ref .read(llmServiceProvider) .listAvailableModels( provider: _selectedProvider, apiKey: _apiKeyController.text.trim().isEmpty ? null : _apiKeyController.text.trim(), baseUrl: _baseUrlController.text.trim(), ); if (!mounted) { return; } setState(() { _models = models; if (models.isNotEmpty && !_models.any( (LlmModelInfo model) => model.id == _modelController.text, )) { _modelController.text = models.first.id; } _modelLoadMessage = models.isEmpty ? 'No models were returned by this provider.' : 'Loaded ${models.length} models.'; }); } catch (error) { if (!mounted) { return; } setState(() { _modelLoadMessage = error.toString(); }); } finally { if (mounted) { setState(() { _loadingModels = false; }); } } } } class _LlmDebugDialog extends ConsumerStatefulWidget { const _LlmDebugDialog(); @override ConsumerState<_LlmDebugDialog> createState() => _LlmDebugDialogState(); } class _LlmDebugDialogState extends ConsumerState<_LlmDebugDialog> { late final TextEditingController _promptController; bool _running = false; String? _response; String? _error; @override void initState() { super.initState(); _promptController = TextEditingController( text: 'Reply with one short sentence confirming the connection works.', ); } @override void dispose() { _promptController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final LlmConfigState config = ref.watch(llmConfigProvider); return AlertDialog( title: const Text('LLM Debug'), content: SizedBox( width: 560, child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ _LlmDebugConnectionSummary(config: config), const SizedBox(height: 12), TextField( controller: _promptController, minLines: 3, maxLines: 5, decoration: const InputDecoration( labelText: 'Test prompt', border: OutlineInputBorder(), ), ), const SizedBox(height: 12), if (_running) const LinearProgressIndicator(), if (_error != null) ...[ const SizedBox(height: 12), SelectableText( _error!, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Theme.of(context).colorScheme.error, ), ), ], if (_response != null) ...[ const SizedBox(height: 12), DecoratedBox( decoration: BoxDecoration( border: Border.all(color: Theme.of(context).dividerColor), borderRadius: BorderRadius.circular(6), ), child: Padding( padding: const EdgeInsets.all(12), child: Align( alignment: Alignment.centerLeft, child: SelectableText(_response!), ), ), ), ], ], ), ), ), actions: [ TextButton( onPressed: _running ? null : () => Navigator.of(context).pop(), child: const Text('Close'), ), FilledButton.icon( onPressed: !_running && config.isConfigured ? _runDebugPrompt : null, icon: const Icon(Icons.play_arrow_rounded), label: const Text('Send Test'), ), ], ); } Future _runDebugPrompt() async { setState(() { _running = true; _response = null; _error = null; }); try { final String response = await ref .read(llmServiceProvider) .completeText( systemPrompt: 'You are a connectivity probe for a relationship app. Answer briefly.', userPrompt: _promptController.text.trim(), ); if (!mounted) { return; } setState(() { _response = response; }); } catch (error) { if (!mounted) { return; } setState(() { _error = error.toString(); }); } finally { if (mounted) { setState(() { _running = false; }); } } } } class _LlmDebugConnectionSummary extends StatelessWidget { const _LlmDebugConnectionSummary({required this.config}); final LlmConfigState config; @override Widget build(BuildContext context) { final TextStyle? labelStyle = Theme.of( context, ).textTheme.bodySmall?.copyWith(color: AppTheme.textSecondary); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(config.isConfigured ? 'Configured' : 'Not configured'), const SizedBox(height: 6), Text('Provider: ${config.provider.label}', style: labelStyle), Text( 'Model: ${config.model.isEmpty ? 'Not selected' : config.model}', style: labelStyle, ), Text('Endpoint: ${config.baseUrl}', style: labelStyle), ], ); } } class _ShareSimulationDraft { const _ShareSimulationDraft({ required this.rawText, required this.sourceAppHint, }); final String rawText; final String sourceAppHint; } class _ShareSimulationDialog extends StatefulWidget { const _ShareSimulationDialog(); @override State<_ShareSimulationDialog> createState() => _ShareSimulationDialogState(); } class _ShareSimulationDialogState extends State<_ShareSimulationDialog> { late final TextEditingController _controller; String _selectedSourceApp = 'whatsapp'; @override void initState() { super.initState(); _controller = TextEditingController( text: '[2/18/26, 20:11] Ava Hart: Can we do coffee this Saturday?', ); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return AlertDialog( title: const Text('Simulate Share Capture'), content: Column( mainAxisSize: MainAxisSize.min, children: [ DropdownButtonFormField( initialValue: _selectedSourceApp, decoration: const InputDecoration(labelText: 'Source app'), items: const >[ DropdownMenuItem( value: 'whatsapp', child: Text('WhatsApp'), ), DropdownMenuItem(value: 'signal', child: Text('Signal')), DropdownMenuItem(value: 'notes', child: Text('Notes')), DropdownMenuItem( value: 'share_sheet', child: Text('Generic text/link'), ), ], onChanged: (String? value) { if (value == null) { return; } setState(() { _selectedSourceApp = value; }); }, ), const SizedBox(height: 12), TextField( controller: _controller, minLines: 3, maxLines: 6, decoration: const InputDecoration( hintText: 'Paste shared text or URL payload', ), ), ], ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('Cancel'), ), FilledButton( onPressed: () { final String rawText = _controller.text.trim(); if (rawText.isEmpty) { return; } Navigator.of(context).pop( _ShareSimulationDraft( rawText: rawText, sourceAppHint: _selectedSourceApp, ), ); }, child: const Text('Import'), ), ], ); } }