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
}
@@ -6,11 +6,19 @@ import FlutterMacOS
import Foundation
import device_info_plus
<<<<<<< HEAD
=======
import path_provider_foundation
>>>>>>> c5a85e05a3bb50cd4ea7de0da2db94c867eb7ddd
import shared_preferences_foundation
import sqflite_darwin
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
<<<<<<< HEAD
=======
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
>>>>>>> c5a85e05a3bb50cd4ea7de0da2db94c867eb7ddd
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
}
+34
View File
@@ -416,6 +416,7 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.8"
<<<<<<< HEAD
pub_semver:
dependency: transitive
description:
@@ -432,28 +433,45 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.6.0"
=======
>>>>>>> c5a85e05a3bb50cd4ea7de0da2db94c867eb7ddd
shared_preferences:
dependency: "direct main"
description:
name: shared_preferences
sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf
<<<<<<< HEAD
url: "https://pub.flutter-io.cn"
=======
url: "https://pub.dev"
>>>>>>> c5a85e05a3bb50cd4ea7de0da2db94c867eb7ddd
source: hosted
version: "2.5.5"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
<<<<<<< HEAD
sha256: "93ae5884a9df5d3bb696825bceb3a17590754548b5d740eba51500afc8d088f5"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.26"
=======
sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53
url: "https://pub.dev"
source: hosted
version: "2.4.23"
>>>>>>> c5a85e05a3bb50cd4ea7de0da2db94c867eb7ddd
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f"
<<<<<<< HEAD
url: "https://pub.flutter-io.cn"
=======
url: "https://pub.dev"
>>>>>>> c5a85e05a3bb50cd4ea7de0da2db94c867eb7ddd
source: hosted
version: "2.5.6"
shared_preferences_linux:
@@ -461,7 +479,11 @@ packages:
description:
name: shared_preferences_linux
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
<<<<<<< HEAD
url: "https://pub.flutter-io.cn"
=======
url: "https://pub.dev"
>>>>>>> c5a85e05a3bb50cd4ea7de0da2db94c867eb7ddd
source: hosted
version: "2.4.1"
shared_preferences_platform_interface:
@@ -469,7 +491,11 @@ packages:
description:
name: shared_preferences_platform_interface
sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9"
<<<<<<< HEAD
url: "https://pub.flutter-io.cn"
=======
url: "https://pub.dev"
>>>>>>> c5a85e05a3bb50cd4ea7de0da2db94c867eb7ddd
source: hosted
version: "2.4.2"
shared_preferences_web:
@@ -477,7 +503,11 @@ packages:
description:
name: shared_preferences_web
sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019
<<<<<<< HEAD
url: "https://pub.flutter-io.cn"
=======
url: "https://pub.dev"
>>>>>>> c5a85e05a3bb50cd4ea7de0da2db94c867eb7ddd
source: hosted
version: "2.4.3"
shared_preferences_windows:
@@ -485,7 +515,11 @@ packages:
description:
name: shared_preferences_windows
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
<<<<<<< HEAD
url: "https://pub.flutter-io.cn"
=======
url: "https://pub.dev"
>>>>>>> c5a85e05a3bb50cd4ea7de0da2db94c867eb7ddd
source: hosted
version: "2.4.1"
sky_engine:
+4
View File
@@ -38,8 +38,12 @@ dependencies:
path: ^1.9.0
flutter_svg: ^2.0.10+1
shared_preferences: ^2.2.0
<<<<<<< HEAD
file_picker: ^6.0.0
=======
flutter_saf: ^0.1.0
>>>>>>> c5a85e05a3bb50cd4ea7de0da2db94c867eb7ddd
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.