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_provider/path_provider.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import 'package:device_info_plus/device_info_plus.dart'; import 'package:device_info_plus/device_info_plus.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../models/device_matching.dart'; import '../models/device_matching.dart';
import '../utils/charge_utils.dart'; import '../utils/charge_utils.dart';
@@ -250,6 +251,10 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
// ---------- SAF 授权引导 ---------- // ---------- SAF 授权引导 ----------
void _showSafAuthorizationDialog() { void _showSafAuthorizationDialog() {
<<<<<<< HEAD
=======
// 避免重复弹窗
>>>>>>> c5a85e05a3bb50cd4ea7de0da2db94c867eb7ddd
if (!mounted) return; if (!mounted) return;
showDialog( showDialog(
@@ -275,6 +280,10 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
TextButton( TextButton(
onPressed: () { onPressed: () {
Navigator.pop(context); Navigator.pop(context);
<<<<<<< HEAD
=======
// 用户取消后,保持当前状态,但不阻塞后续操作
>>>>>>> c5a85e05a3bb50cd4ea7de0da2db94c867eb7ddd
setState(() { setState(() {
_safState = SafPermissionState.denied; _safState = SafPermissionState.denied;
}); });
@@ -283,11 +292,19 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
), ),
ElevatedButton( ElevatedButton(
onPressed: () async { onPressed: () async {
<<<<<<< HEAD
final ctx = context; final ctx = context;
Navigator.pop(ctx); Navigator.pop(ctx);
final result = await _safService.requestPermission(); final result = await _safService.requestPermission();
if (result.success) { if (result.success) {
if (!mounted) return; if (!mounted) return;
=======
final ctx = context; // 保存引用
Navigator.pop(ctx);
final result = await _safService.requestPermission();
if (result.success) {
if (!mounted) return; // 先检查 State 是否还在
>>>>>>> c5a85e05a3bb50cd4ea7de0da2db94c867eb7ddd
setState(() { setState(() {
_safState = SafPermissionState.authorized; _safState = SafPermissionState.authorized;
}); });
@@ -300,10 +317,17 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
backgroundColor: Colors.red, backgroundColor: Colors.red,
), ),
); );
<<<<<<< HEAD
_showSafAuthorizationDialog(); _showSafAuthorizationDialog();
} }
}, },
child: const Text('选择文件夹'), // 🟢 加这行 child: const Text('选择文件夹'), // 🟢 加这行
=======
// 重新弹窗
_showSafAuthorizationDialog();
}
},
>>>>>>> c5a85e05a3bb50cd4ea7de0da2db94c867eb7ddd
), ),
], ],
), ),
+146
View File
@@ -1,15 +1,24 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
<<<<<<< HEAD
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import '../models/saf_permission_state.dart'; import '../models/saf_permission_state.dart';
/// 使用 file_picker 实现文件夹访问 /// 使用 file_picker 实现文件夹访问
=======
import 'package:flutter_saf/flutter_saf.dart';
import '../models/saf_permission_state.dart';
/// SAF 存储服务
/// 负责管理通过 SAF 授权的文件夹访问
>>>>>>> c5a85e05a3bb50cd4ea7de0da2db94c867eb7ddd
class SafStorageService { class SafStorageService {
static final SafStorageService _instance = SafStorageService._internal(); static final SafStorageService _instance = SafStorageService._internal();
factory SafStorageService() => _instance; factory SafStorageService() => _instance;
SafStorageService._internal(); SafStorageService._internal();
<<<<<<< HEAD
static const String _prefKeySafPath = 'saf_storage_path'; static const String _prefKeySafPath = 'saf_storage_path';
String? _cachedPath; String? _cachedPath;
@@ -28,6 +37,34 @@ class SafStorageService {
try { try {
final dir = Directory(_cachedPath!); final dir = Directory(_cachedPath!);
if (!await dir.exists()) { 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(); _clearCache();
return SafPermissionState.expired; return SafPermissionState.expired;
} }
@@ -38,6 +75,7 @@ class SafStorageService {
} }
} }
<<<<<<< HEAD
Future<SafAuthorizationResult> requestPermission() async { Future<SafAuthorizationResult> requestPermission() async {
try { try {
// 使用 file_picker 选择文件夹 // 使用 file_picker 选择文件夹
@@ -60,6 +98,40 @@ class SafStorageService {
} }
return SafAuthorizationResult.success(selectedPath); 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) { } catch (e) {
if (kDebugMode) { if (kDebugMode) {
print('[SAF] 授权失败: $e'); print('[SAF] 授权失败: $e');
@@ -68,17 +140,32 @@ class SafStorageService {
} }
} }
<<<<<<< HEAD
Future<bool> logFileExists(String fileName) async { Future<bool> logFileExists(String fileName) async {
final state = await getPermissionState(); final state = await getPermissionState();
if (state != SafPermissionState.authorized) return false; if (state != SafPermissionState.authorized) return false;
try { try {
final file = File('${_cachedPath!}/$fileName'); final file = File('${_cachedPath!}/$fileName');
return await file.exists(); 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) { } catch (e) {
return false; return false;
} }
} }
<<<<<<< HEAD
Future<List<String>> readLogFile(String fileName) async { Future<List<String>> readLogFile(String fileName) async {
final state = await getPermissionState(); final state = await getPermissionState();
if (state != SafPermissionState.authorized) return []; if (state != SafPermissionState.authorized) return [];
@@ -86,6 +173,25 @@ class SafStorageService {
final file = File('${_cachedPath!}/$fileName'); final file = File('${_cachedPath!}/$fileName');
if (!await file.exists()) return []; if (!await file.exists()) return [];
return await file.readAsLines(); 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) { } catch (e) {
if (kDebugMode) { if (kDebugMode) {
print('[SAF] 读取文件失败: $e'); print('[SAF] 读取文件失败: $e');
@@ -94,6 +200,7 @@ class SafStorageService {
} }
} }
<<<<<<< HEAD
Future<List<String>> listFiles() async { Future<List<String>> listFiles() async {
final state = await getPermissionState(); final state = await getPermissionState();
if (state != SafPermissionState.authorized) return []; if (state != SafPermissionState.authorized) return [];
@@ -109,15 +216,34 @@ class SafStorageService {
if (kDebugMode) { if (kDebugMode) {
print('[SAF] 列出文件失败: $e'); 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 []; return [];
} }
} }
<<<<<<< HEAD
=======
/// 检查授权是否有效,如果失效则清除缓存
>>>>>>> c5a85e05a3bb50cd4ea7de0da2db94c867eb7ddd
Future<bool> validatePermission() async { Future<bool> validatePermission() async {
final state = await getPermissionState(); final state = await getPermissionState();
return state == SafPermissionState.authorized; return state == SafPermissionState.authorized;
} }
<<<<<<< HEAD
void _clearCache() { void _clearCache() {
_cachedPath = null; _cachedPath = null;
_savePath(null); _savePath(null);
@@ -133,4 +259,24 @@ class SafStorageService {
} }
String? get cachedPath => _cachedPath; 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
} }
@@ -6,11 +6,19 @@ import FlutterMacOS
import Foundation import Foundation
import device_info_plus import device_info_plus
<<<<<<< HEAD
=======
import path_provider_foundation
>>>>>>> c5a85e05a3bb50cd4ea7de0da2db94c867eb7ddd
import shared_preferences_foundation import shared_preferences_foundation
import sqflite_darwin import sqflite_darwin
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
<<<<<<< HEAD
=======
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
>>>>>>> c5a85e05a3bb50cd4ea7de0da2db94c867eb7ddd
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
} }
+34
View File
@@ -416,6 +416,7 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.1.8" version: "2.1.8"
<<<<<<< HEAD
pub_semver: pub_semver:
dependency: transitive dependency: transitive
description: description:
@@ -432,28 +433,45 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "0.6.0" version: "0.6.0"
=======
>>>>>>> c5a85e05a3bb50cd4ea7de0da2db94c867eb7ddd
shared_preferences: shared_preferences:
dependency: "direct main" dependency: "direct main"
description: description:
name: shared_preferences name: shared_preferences
sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf
<<<<<<< HEAD
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
=======
url: "https://pub.dev"
>>>>>>> c5a85e05a3bb50cd4ea7de0da2db94c867eb7ddd
source: hosted source: hosted
version: "2.5.5" version: "2.5.5"
shared_preferences_android: shared_preferences_android:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_android name: shared_preferences_android
<<<<<<< HEAD
sha256: "93ae5884a9df5d3bb696825bceb3a17590754548b5d740eba51500afc8d088f5" sha256: "93ae5884a9df5d3bb696825bceb3a17590754548b5d740eba51500afc8d088f5"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.4.26" version: "2.4.26"
=======
sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53
url: "https://pub.dev"
source: hosted
version: "2.4.23"
>>>>>>> c5a85e05a3bb50cd4ea7de0da2db94c867eb7ddd
shared_preferences_foundation: shared_preferences_foundation:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_foundation name: shared_preferences_foundation
sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f"
<<<<<<< HEAD
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
=======
url: "https://pub.dev"
>>>>>>> c5a85e05a3bb50cd4ea7de0da2db94c867eb7ddd
source: hosted source: hosted
version: "2.5.6" version: "2.5.6"
shared_preferences_linux: shared_preferences_linux:
@@ -461,7 +479,11 @@ packages:
description: description:
name: shared_preferences_linux name: shared_preferences_linux
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
<<<<<<< HEAD
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
=======
url: "https://pub.dev"
>>>>>>> c5a85e05a3bb50cd4ea7de0da2db94c867eb7ddd
source: hosted source: hosted
version: "2.4.1" version: "2.4.1"
shared_preferences_platform_interface: shared_preferences_platform_interface:
@@ -469,7 +491,11 @@ packages:
description: description:
name: shared_preferences_platform_interface name: shared_preferences_platform_interface
sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9" sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9"
<<<<<<< HEAD
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
=======
url: "https://pub.dev"
>>>>>>> c5a85e05a3bb50cd4ea7de0da2db94c867eb7ddd
source: hosted source: hosted
version: "2.4.2" version: "2.4.2"
shared_preferences_web: shared_preferences_web:
@@ -477,7 +503,11 @@ packages:
description: description:
name: shared_preferences_web name: shared_preferences_web
sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019
<<<<<<< HEAD
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
=======
url: "https://pub.dev"
>>>>>>> c5a85e05a3bb50cd4ea7de0da2db94c867eb7ddd
source: hosted source: hosted
version: "2.4.3" version: "2.4.3"
shared_preferences_windows: shared_preferences_windows:
@@ -485,7 +515,11 @@ packages:
description: description:
name: shared_preferences_windows name: shared_preferences_windows
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
<<<<<<< HEAD
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
=======
url: "https://pub.dev"
>>>>>>> c5a85e05a3bb50cd4ea7de0da2db94c867eb7ddd
source: hosted source: hosted
version: "2.4.1" version: "2.4.1"
sky_engine: sky_engine:
+4
View File
@@ -38,8 +38,12 @@ dependencies:
path: ^1.9.0 path: ^1.9.0
flutter_svg: ^2.0.10+1 flutter_svg: ^2.0.10+1
shared_preferences: ^2.2.0 shared_preferences: ^2.2.0
<<<<<<< HEAD
file_picker: ^6.0.0 file_picker: ^6.0.0
=======
flutter_saf: ^0.1.0
>>>>>>> c5a85e05a3bb50cd4ea7de0da2db94c867eb7ddd
# The following adds the Cupertino Icons font to your application. # The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons. # Use with the CupertinoIcons class for iOS style icons.