This commit is contained in:
2026-07-04 22:38:27 +08:00
5 changed files with 216 additions and 0 deletions
+24
View File
@@ -8,6 +8,7 @@ import 'package:sqflite/sqflite.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as path;
import 'package:device_info_plus/device_info_plus.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../models/device_matching.dart';
import '../utils/charge_utils.dart';
@@ -250,6 +251,10 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
// ---------- SAF 授权引导 ----------
void _showSafAuthorizationDialog() {
<<<<<<< HEAD
=======
// 避免重复弹窗
>>>>>>> c5a85e05a3bb50cd4ea7de0da2db94c867eb7ddd
if (!mounted) return;
showDialog(
@@ -275,6 +280,10 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
TextButton(
onPressed: () {
Navigator.pop(context);
<<<<<<< HEAD
=======
// 用户取消后,保持当前状态,但不阻塞后续操作
>>>>>>> c5a85e05a3bb50cd4ea7de0da2db94c867eb7ddd
setState(() {
_safState = SafPermissionState.denied;
});
@@ -283,11 +292,19 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
),
ElevatedButton(
onPressed: () async {
<<<<<<< HEAD
final ctx = context;
Navigator.pop(ctx);
final result = await _safService.requestPermission();
if (result.success) {
if (!mounted) return;
=======
final ctx = context; // 保存引用
Navigator.pop(ctx);
final result = await _safService.requestPermission();
if (result.success) {
if (!mounted) return; // 先检查 State 是否还在
>>>>>>> c5a85e05a3bb50cd4ea7de0da2db94c867eb7ddd
setState(() {
_safState = SafPermissionState.authorized;
});
@@ -300,10 +317,17 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
backgroundColor: Colors.red,
),
);
<<<<<<< HEAD
_showSafAuthorizationDialog();
}
},
child: const Text('选择文件夹'), // 🟢 加这行
=======
// 重新弹窗
_showSafAuthorizationDialog();
}
},
>>>>>>> c5a85e05a3bb50cd4ea7de0da2db94c867eb7ddd
),
],
),
+146
View File
@@ -1,15 +1,24 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
<<<<<<< HEAD
import 'package:file_picker/file_picker.dart';
import '../models/saf_permission_state.dart';
/// 使用 file_picker 实现文件夹访问
=======
import 'package:flutter_saf/flutter_saf.dart';
import '../models/saf_permission_state.dart';
/// SAF 存储服务
/// 负责管理通过 SAF 授权的文件夹访问
>>>>>>> c5a85e05a3bb50cd4ea7de0da2db94c867eb7ddd
class SafStorageService {
static final SafStorageService _instance = SafStorageService._internal();
factory SafStorageService() => _instance;
SafStorageService._internal();
<<<<<<< HEAD
static const String _prefKeySafPath = 'saf_storage_path';
String? _cachedPath;
@@ -28,6 +37,34 @@ class SafStorageService {
try {
final dir = Directory(_cachedPath!);
if (!await dir.exists()) {
=======
static const String _prefKeySafUri = 'saf_storage_uri';
static const String _prefKeySafPermission = 'saf_permission_uri';
/// 存储的授权 URI
String? _cachedUriString;
/// 初始化,从 SharedPreferences 恢复授权状态
Future<void> init() async {
final prefs = await SharedPreferences.getInstance();
_cachedUriString = prefs.getString(_prefKeySafUri);
if (kDebugMode) {
print('[SAF] 初始化,缓存 URI: $_cachedUriString');
}
}
/// 获取当前授权状态
Future<SafPermissionState> getPermissionState() async {
if (_cachedUriString == null || _cachedUriString!.isEmpty) {
return SafPermissionState.unauthorized;
}
try {
final uri = Uri.parse(_cachedUriString!);
// 检查 Uri 是否仍然有效
final isValid = await FlutterSaf.isDirectory(uri);
if (!isValid) {
>>>>>>> c5a85e05a3bb50cd4ea7de0da2db94c867eb7ddd
_clearCache();
return SafPermissionState.expired;
}
@@ -38,6 +75,7 @@ class SafStorageService {
}
}
<<<<<<< HEAD
Future<SafAuthorizationResult> requestPermission() async {
try {
// 使用 file_picker 选择文件夹
@@ -60,6 +98,40 @@ class SafStorageService {
}
return SafAuthorizationResult.success(selectedPath);
=======
/// 请求 SAF 授权(引导用户选择文件夹)
/// 返回授权结果
Future<SafAuthorizationResult> requestPermission() async {
try {
// 调用系统文件夹选择器
// 引导用户选择 /storage/emulated/0/mbattery_charger/
// 或者用户可以选择任何目录,但我们建议选择具体路径
final uri = await FlutterSaf.openDocumentTree();
if (uri == null) {
return SafAuthorizationResult.denied();
}
// 获取持久化 URI 权限
await FlutterSaf.takePersistableUriPermission(uri);
// 验证是否为目录
final isValid = await FlutterSaf.isDirectory(uri);
if (!isValid) {
return SafAuthorizationResult.error('选择的路径不是有效目录');
}
// 保存 URI
final uriString = uri.toString();
_cachedUriString = uriString;
await _saveUri(uriString);
if (kDebugMode) {
print('[SAF] 授权成功,URI: $uriString');
}
return SafAuthorizationResult.success(uriString);
>>>>>>> c5a85e05a3bb50cd4ea7de0da2db94c867eb7ddd
} catch (e) {
if (kDebugMode) {
print('[SAF] 授权失败: $e');
@@ -68,17 +140,32 @@ class SafStorageService {
}
}
<<<<<<< HEAD
Future<bool> logFileExists(String fileName) async {
final state = await getPermissionState();
if (state != SafPermissionState.authorized) return false;
try {
final file = File('${_cachedPath!}/$fileName');
return await file.exists();
=======
/// 检查指定日期的日志文件是否存在
Future<bool> logFileExists(String fileName) async {
final state = await getPermissionState();
if (state != SafPermissionState.authorized) {
return false;
}
try {
final uri = Uri.parse(_cachedUriString!);
final fileUri = await FlutterSaf.getFile(uri, fileName);
return fileUri != null && await FlutterSaf.isFile(fileUri);
>>>>>>> c5a85e05a3bb50cd4ea7de0da2db94c867eb7ddd
} catch (e) {
return false;
}
}
<<<<<<< HEAD
Future<List<String>> readLogFile(String fileName) async {
final state = await getPermissionState();
if (state != SafPermissionState.authorized) return [];
@@ -86,6 +173,25 @@ class SafStorageService {
final file = File('${_cachedPath!}/$fileName');
if (!await file.exists()) return [];
return await file.readAsLines();
=======
/// 读取日志文件内容
/// 返回文件行列表,如果文件不存在或读取失败返回空列表
Future<List<String>> readLogFile(String fileName) async {
final state = await getPermissionState();
if (state != SafPermissionState.authorized) {
return [];
}
try {
final uri = Uri.parse(_cachedUriString!);
final fileUri = await FlutterSaf.getFile(uri, fileName);
if (fileUri == null || !await FlutterSaf.isFile(fileUri)) {
return [];
}
final content = await FlutterSaf.readTextFile(fileUri);
return content.split('\n');
>>>>>>> c5a85e05a3bb50cd4ea7de0da2db94c867eb7ddd
} catch (e) {
if (kDebugMode) {
print('[SAF] 读取文件失败: $e');
@@ -94,6 +200,7 @@ class SafStorageService {
}
}
<<<<<<< HEAD
Future<List<String>> listFiles() async {
final state = await getPermissionState();
if (state != SafPermissionState.authorized) return [];
@@ -109,15 +216,34 @@ class SafStorageService {
if (kDebugMode) {
print('[SAF] 列出文件失败: $e');
}
=======
/// 获取文件夹中的文件列表
Future<List<String>> listFiles() async {
final state = await getPermissionState();
if (state != SafPermissionState.authorized) {
return [];
}
try {
final uri = Uri.parse(_cachedUriString!);
final files = await FlutterSaf.listFiles(uri);
return files.where((f) => f.isFile).map((f) => f.name).toList();
} catch (e) {
>>>>>>> c5a85e05a3bb50cd4ea7de0da2db94c867eb7ddd
return [];
}
}
<<<<<<< HEAD
=======
/// 检查授权是否有效,如果失效则清除缓存
>>>>>>> c5a85e05a3bb50cd4ea7de0da2db94c867eb7ddd
Future<bool> validatePermission() async {
final state = await getPermissionState();
return state == SafPermissionState.authorized;
}
<<<<<<< HEAD
void _clearCache() {
_cachedPath = null;
_savePath(null);
@@ -133,4 +259,24 @@ class SafStorageService {
}
String? get cachedPath => _cachedPath;
=======
/// 清除授权缓存
void _clearCache() {
_cachedUriString = null;
_saveUri(null);
}
/// 保存 URI 到 SharedPreferences
Future<void> _saveUri(String? uriString) async {
final prefs = await SharedPreferences.getInstance();
if (uriString != null && uriString.isNotEmpty) {
await prefs.setString(_prefKeySafUri, uriString);
} else {
await prefs.remove(_prefKeySafUri);
}
}
/// 获取缓存的 URI(用于调试)
String? get cachedUri => _cachedUriString;
>>>>>>> c5a85e05a3bb50cd4ea7de0da2db94c867eb7ddd
}