Files
transfer_sms/app/lib/screens/sms_list_screen.dart
2026-04-28 22:36:55 +08:00

287 lines
9.4 KiB
Dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
import '../models/sms_message.dart';
import '../main.dart';
import '../services/sms_foreground_handler.dart';
class SmsListScreen extends StatefulWidget {
const SmsListScreen({super.key});
@override
State<SmsListScreen> createState() => _SmsListScreenState();
}
class _SmsListScreenState extends State<SmsListScreen> {
List<SmsMessage> _messages = [];
final _selected = <int>{};
Set<String> _uploadedKeys = {};
bool _loading = false;
bool _hasSmsPermission = false;
StreamSubscription<SmsMessage>? _smsSub;
static String _dedupKey(SmsMessage m) => '${m.phoneNumber}|${m.content}|${m.smsDate}';
@override
void initState() {
super.initState();
_requestPermission();
}
Future<void> _requestPermission() async {
final status = await Permission.sms.request();
if (mounted) {
setState(() => _hasSmsPermission = status.isGranted);
if (status.isGranted) {
_loadSms();
if (settings.autoUpload) _startAutoUpload();
}
}
}
Future<void> _loadSms() async {
setState(() => _loading = true);
try {
final local = await smsReaderService.queryAllSms();
Set<String> serverKeys = {};
try {
final serverMsgs = await apiService.fetchServerSms();
serverKeys = serverMsgs.map((m) {
final phone = m['phone_number'] as String? ?? '';
final content = m['content'] as String? ?? '';
final date = m['sms_date'] as String? ?? '';
return '$phone|$content|$date';
}).toSet();
} catch (_) {}
setState(() {
_messages = local;
_uploadedKeys = serverKeys;
});
} catch (e) { /* ignore */ }
if (mounted) setState(() => _loading = false);
}
bool _isUploaded(SmsMessage m) => _uploadedKeys.contains(_dedupKey(m));
Future<void> _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).toSet();
setState(() {
_uploadedKeys = _uploadedKeys.union(newKeys);
_selected.clear();
});
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('已上传 ${toUpload.length} 条短信')),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('上传失败: $e')),
);
}
}
}
Future<void> _uploadAll() async {
final toUpload = _messages.where((m) => !_isUploaded(m)).toList();
if (toUpload.isEmpty) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('没有新短信需要上传')),
);
}
return;
}
try {
await apiService.uploadSms(toUpload);
final newKeys = toUpload.map(_dedupKey).toSet();
setState(() {
_uploadedKeys = _uploadedKeys.union(newKeys);
});
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('已上传 ${toUpload.length} 条短信')),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('上传失败: $e')),
);
}
}
}
Future<void> _toggleAutoUpload() async {
final newVal = !settings.autoUpload;
settings.setAutoUpload(newVal);
if (newVal) {
// Request notification permission for foreground service
final notifResult = await FlutterForegroundTask.requestNotificationPermission();
if (notifResult != NotificationPermission.granted) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('需要通知权限才能保持后台运行')),
);
}
settings.setAutoUpload(false);
setState(() {});
return;
}
await _startForegroundService();
_startAutoUpload();
} else {
_stopForegroundService();
_smsSub?.cancel();
_smsSub = null;
}
setState(() {});
}
Future<void> _startForegroundService() async {
final result = await FlutterForegroundTask.startService(
serviceId: 888,
notificationTitle: 'SMS Monitor',
notificationText: '正在监听新短信...',
callback: startCallback,
);
if (result is ServiceRequestFailure && mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('后台服务启动失败: ${result.error}')),
);
settings.setAutoUpload(false);
setState(() {});
}
}
void _stopForegroundService() {
FlutterForegroundTask.stopService();
}
void _startAutoUpload() {
_smsSub = smsReaderService.listenToIncoming().listen((sms) async {
try {
await apiService.uploadSms([sms]);
setState(() {
_uploadedKeys.add(_dedupKey(sms));
_messages.insert(0, sms);
});
} 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('短信'),
actions: [
if (_selected.isNotEmpty)
IconButton(icon: const Icon(Icons.cloud_upload), onPressed: _uploadSelected, tooltip: '上传选中'),
IconButton(icon: const Icon(Icons.refresh), onPressed: _hasSmsPermission ? _loadSms : _requestPermission),
],
),
body: _hasSmsPermission == false
? Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text('需要短信权限'),
const SizedBox(height: 12),
FilledButton(onPressed: _requestPermission, child: const Text('授权')),
],
),
)
: _loading
? const Center(child: CircularProgressIndicator())
: _messages.isEmpty
? const Center(child: Text('暂无短信'))
: 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: '全部上传',
child: const Icon(Icons.cloud_upload),
),
],
),
);
}
}