import 'dart:async'; import 'package:flutter/material.dart'; import 'package:permission_handler/permission_handler.dart'; import '../models/sms_message.dart'; import '../main.dart'; class SmsListScreen extends StatefulWidget { const SmsListScreen({super.key}); @override State createState() => _SmsListScreenState(); } class _SmsListScreenState extends State { List _messages = []; final _selected = {}; Set _uploadedKeys = {}; bool _loading = false; bool _hasSmsPermission = false; StreamSubscription? _smsSub; static String _dedupKey(SmsMessage m) => '${m.phoneNumber}|${m.content}|${m.smsDate}'; @override void initState() { super.initState(); _uploadedKeys = settings.getUploadedKeys(); _requestPermission(); } Future _requestPermission() async { final status = await Permission.sms.request(); if (mounted) { setState(() => _hasSmsPermission = status.isGranted); if (status.isGranted) _loadSms(); } } Future _loadSms() async { setState(() => _loading = true); try { _messages = await smsReaderService.queryAllSms(); } catch (e) { /* ignore */ } setState(() => _loading = false); } bool _isUploaded(SmsMessage m) => _uploadedKeys.contains(_dedupKey(m)); Future _uploadSelected() async { if (_selected.isEmpty) return; final toUpload = _messages .asMap() .entries .where((e) => _selected.contains(e.key) && !_isUploaded(e.value)) .map((e) => e.value) .toList(); if (toUpload.isEmpty) return; try { await apiService.uploadSms(toUpload); final newKeys = toUpload.map(_dedupKey).toList(); settings.addUploadedKeys(newKeys); _uploadedKeys = settings.getUploadedKeys(); _selected.clear(); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Uploaded ${toUpload.length} messages')), ); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Upload failed: $e')), ); } } } Future _uploadAll() async { final toUpload = _messages.where((m) => !_isUploaded(m)).toList(); if (toUpload.isEmpty) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('No new messages to upload')), ); } return; } try { final count = await apiService.uploadSms(toUpload); final newKeys = toUpload.map(_dedupKey).toList(); settings.addUploadedKeys(newKeys); _uploadedKeys = settings.getUploadedKeys(); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Uploaded $count messages')), ); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Upload failed: $e')), ); } } } void _toggleAutoUpload() { final newVal = !settings.autoUpload; settings.setAutoUpload(newVal); if (newVal) { _startAutoUpload(); } else { _smsSub?.cancel(); _smsSub = null; } setState(() {}); } void _startAutoUpload() { _smsSub = smsReaderService.listenToIncoming().listen((sms) async { try { await apiService.uploadSms([sms]); settings.addUploadedKey(_dedupKey(sms)); _uploadedKeys = settings.getUploadedKeys(); _messages.insert(0, sms); if (mounted) setState(() {}); } catch (_) {} }); } @override void dispose() { _smsSub?.cancel(); super.dispose(); } @override Widget build(BuildContext context) { final colors = Theme.of(context).colorScheme; return Scaffold( appBar: AppBar( title: const Text('Messages'), actions: [ if (_selected.isNotEmpty) IconButton(icon: const Icon(Icons.cloud_upload), onPressed: _uploadSelected, tooltip: 'Upload selected'), IconButton(icon: const Icon(Icons.refresh), onPressed: _hasSmsPermission ? _loadSms : _requestPermission), ], ), body: _hasSmsPermission == false ? Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ const Text('SMS permission required'), const SizedBox(height: 12), FilledButton(onPressed: _requestPermission, child: const Text('Grant')), ], ), ) : _loading ? const Center(child: CircularProgressIndicator()) : _messages.isEmpty ? const Center(child: Text('No messages')) : RefreshIndicator( onRefresh: _loadSms, child: ListView.builder( itemCount: _messages.length, itemBuilder: (_, i) { final m = _messages[i]; final sel = _selected.contains(i); final uploaded = _isUploaded(m); return ListTile( selected: sel, leading: CircleAvatar( backgroundColor: sel ? colors.primaryContainer : colors.surfaceContainerHighest, child: Icon( uploaded ? Icons.cloud_done : (m.type == 'received' ? Icons.arrow_downward : Icons.arrow_upward), size: 18, ), ), title: Text( m.contactName ?? m.phoneNumber, maxLines: 1, overflow: TextOverflow.ellipsis, ), subtitle: Text(m.content, maxLines: 1, overflow: TextOverflow.ellipsis), trailing: uploaded ? Icon(Icons.cloud_done, color: colors.primary, size: 20) : IconButton( icon: Icon(sel ? Icons.check_circle : Icons.circle_outlined), onPressed: () { setState(() { sel ? _selected.remove(i) : _selected.add(i); }); }, ), onTap: uploaded ? null : () { setState(() { sel ? _selected.remove(i) : _selected.add(i); }); }, ); }, ), ), floatingActionButton: Column( mainAxisSize: MainAxisSize.min, children: [ FloatingActionButton.small( heroTag: 'auto', onPressed: _toggleAutoUpload, backgroundColor: settings.autoUpload ? colors.errorContainer : colors.surfaceContainerHighest, child: Icon(settings.autoUpload ? Icons.sync_disabled : Icons.sync, color: colors.onPrimaryContainer), ), const SizedBox(height: 8), FloatingActionButton( heroTag: 'upload', onPressed: _uploadAll, tooltip: 'Upload all', child: const Icon(Icons.cloud_upload), ), ], ), ); } }