269 lines
8.7 KiB
Dart
269 lines
8.7 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
|
|
await Permission.notification.request();
|
|
_startForegroundService();
|
|
_startAutoUpload();
|
|
} else {
|
|
_stopForegroundService();
|
|
_smsSub?.cancel();
|
|
_smsSub = null;
|
|
}
|
|
setState(() {});
|
|
}
|
|
|
|
void _startForegroundService() {
|
|
FlutterForegroundTask.startService(
|
|
notificationTitle: 'SMS Monitor',
|
|
notificationText: '正在监听新短信...',
|
|
callback: startCallback,
|
|
);
|
|
}
|
|
|
|
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),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|