SAF初步修改,等待验证

This commit is contained in:
2026-07-04 20:57:29 +08:00
parent bdf24f6790
commit c5a85e05a3
8 changed files with 432 additions and 53 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" /> <!--<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />-->
<application <application
android:label="@string/app_name" android:label="@string/app_name"
android:name="${applicationName}" android:name="${applicationName}"
+7 -1
View File
@@ -1,7 +1,13 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'pages/battery_health_page.dart'; import 'pages/battery_health_page.dart';
import 'services/saf_storage_service.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// 🟢 初始化 SAF 服务
await SafStorageService().init();
void main() {
runApp(const MyApp()); runApp(const MyApp());
} }
+63
View File
@@ -0,0 +1,63 @@
/// SAF 权限状态
enum SafPermissionState {
/// 未授权(首次使用)
unauthorized,
/// 已授权,Uri 有效
authorized,
/// 授权已过期或 Uri 失效
expired,
/// 用户拒绝了授权
denied,
}
/// SAF 授权结果
class SafAuthorizationResult {
final bool success;
final String? uriString;
final SafPermissionState state;
final String? errorMessage;
SafAuthorizationResult(
{
required this.success,
this.uriString,
required this.state,
this.errorMessage,
}
);
factory SafAuthorizationResult.success(String uri) {
return SafAuthorizationResult(
success: true,
uriString: uri,
state: SafPermissionState.authorized,
);
}
factory SafAuthorizationResult.denied() {
return SafAuthorizationResult(
success: false,
state: SafPermissionState.denied,
errorMessage: '用户拒绝了文件夹访问权限',
);
}
factory SafAuthorizationResult.expired() {
return SafAuthorizationResult(
success: false,
state: SafPermissionState.expired,
errorMessage: '文件夹访问权限已过期,请重新授权',
);
}
factory SafAuthorizationResult.error(String message) {
return SafAuthorizationResult(
success: false,
state: SafPermissionState.unauthorized,
errorMessage: message,
);
}
}
+128 -49
View File
@@ -4,15 +4,17 @@ import 'dart:developer' as dev;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:sqflite/sqflite.dart'; 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';
import '../widgets/charge_detail_page.dart'; import '../widgets/charge_detail_page.dart';
import '../services/saf_storage_service.dart';
import '../models/saf_permission_state.dart';
// --- 潘通色定义 --- // --- 潘通色定义 ---
const Color kHealthGreen = Color(0xFF00B140); const Color kHealthGreen = Color(0xFF00B140);
@@ -40,23 +42,24 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
double _maxVolt = 0; double _maxVolt = 0;
double _minVolt = 0; double _minVolt = 0;
bool _hasData = false; bool _hasData = false;
final String _filePath = '/storage/emulated/0/mbattery_charger/';
// ---------- 趋势图数据 ---------- // ---------- 趋势图数据 ----------
List<Map<String, dynamic>> _trendData = []; List<Map<String, dynamic>> _trendData = [];
// ---------- 充电状态 ---------- // ---------- 充电状态 ----------
bool _isCharging = false; bool _isCharging = false;
String _chargeType = ''; // "EPP", "PPS", "SDP", "无线", "MZ_EPP" 等 String _chargeType = '';
double _chargePower = 0.0; double _chargePower = 0.0;
double _chargeVolt = 0.0; double _chargeVolt = 0.0;
double _chargeCurr = 0.0; double _chargeCurr = 0.0;
int? _wiredLevel; int? _wiredLevel;
bool _isWireless = false; bool _isWireless = false;
// 🟢 1.3.0-dev.3: 充电开始时间(UTC),用于时间窗口判断
DateTime? _chargingStartTime; DateTime? _chargingStartTime;
// ---------- SAF 状态 ----------
final SafStorageService _safService = SafStorageService();
SafPermissionState _safState = SafPermissionState.unauthorized;
// 动画控制 // 动画控制
late AnimationController _chargeAnimationController; late AnimationController _chargeAnimationController;
late Animation<double> _bottomSlide; late Animation<double> _bottomSlide;
@@ -150,22 +153,25 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
} }
} }
// ---------- 扫描目录 ---------- // ---------- 扫描目录 (SAF 模式) ----------
Future<void> _scanAndUpdateDB() async { Future<void> _scanAndUpdateDB() async {
Directory dir = Directory(_filePath); // 检查 SAF 授权状态
if (!await dir.exists()) return; _safState = await _safService.getPermissionState();
if (_safState != SafPermissionState.authorized) {
_showSafAuthorizationDialog();
return;
}
List<FileSystemEntity> files = await dir.list().toList(); try {
final files = await _safService.listFiles();
RegExp filePattern = RegExp(r'mbattery_charger_log_(\d{4}_\d{2}_\d{2})$'); RegExp filePattern = RegExp(r'mbattery_charger_log_(\d{4}_\d{2}_\d{2})$');
for (FileSystemEntity entity in files) { for (String fileName in files) {
if (entity is File) {
String fileName = path.basename(entity.path);
Match? match = filePattern.firstMatch(fileName); Match? match = filePattern.firstMatch(fileName);
if (match != null) { if (match != null) {
String dateStr = match.group(1)!.replaceAll('_', '-'); String dateStr = match.group(1)!.replaceAll('_', '-');
try { try {
List<String> lines = await entity.readAsLines(); List<String> lines = await _safService.readLogFile(fileName);
if (lines.isEmpty) continue; if (lines.isEmpty) continue;
List<String> validLines = lines List<String> validLines = lines
@@ -195,10 +201,12 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
} catch (_) {} } catch (_) {}
} }
} }
} catch (e) {
dev.log('[_scanAndUpdateDB] ❌ 扫描异常: $e', name: 'BatteryHealth');
} }
} }
// ---------- 1.3.0-dev.1: 使用 UTC 生成文件名 ---------- // ---------- UTC 文件名 ----------
String getTodayFileName() { String getTodayFileName() {
DateTime nowUtc = DateTime.now().toUtc(); DateTime nowUtc = DateTime.now().toUtc();
String year = nowUtc.year.toString(); String year = nowUtc.year.toString();
@@ -208,7 +216,6 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
} }
// ---------- 时间戳工具方法 ---------- // ---------- 时间戳工具方法 ----------
/// 解析日志行中的时间戳,强制解析为 UTC
DateTime? _parseLogTimestamp(String line) { DateTime? _parseLogTimestamp(String line) {
RegExp reg = RegExp(r'\[(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2})\]'); RegExp reg = RegExp(r'\[(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2})\]');
Match? match = reg.firstMatch(line); Match? match = reg.firstMatch(line);
@@ -231,20 +238,81 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
} }
} }
/// 🟢 1.3.0-dev.3: 时间窗口以插入时刻为基准,只向前看
bool _isLogTimestampValid(DateTime? logTime) { bool _isLogTimestampValid(DateTime? logTime) {
if (logTime == null) return false; if (logTime == null) return false;
if (_chargingStartTime == null) { if (_chargingStartTime == null) {
// 边缘情况:回退到当前时间 ±3 分钟
DateTime nowUtc = DateTime.now().toUtc(); DateTime nowUtc = DateTime.now().toUtc();
Duration diff = nowUtc.difference(logTime); Duration diff = nowUtc.difference(logTime);
return diff.abs().inMinutes <= 3; return diff.abs().inMinutes <= 3;
} }
// 日志时间必须在 [充电开始时间, 充电开始时间 + 3 分钟] 范围内
DateTime endTime = _chargingStartTime!.add(const Duration(minutes: 3)); DateTime endTime = _chargingStartTime!.add(const Duration(minutes: 3));
return logTime.isAfter(_chargingStartTime!) && logTime.isBefore(endTime); return logTime.isAfter(_chargingStartTime!) && logTime.isBefore(endTime);
} }
// ---------- SAF 授权引导 ----------
void _showSafAuthorizationDialog() {
// 避免重复弹窗
if (!mounted) return;
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => AlertDialog(
title: const Text('需要文件夹访问权限'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text('请授权访问日志文件夹,以读取电池数据。'),
SizedBox(height: 8),
Text(
'目标路径:\n/storage/emulated/0/mbattery_charger/',
style: TextStyle(fontSize: 12, color: Colors.grey),
),
SizedBox(height: 8),
Text('点击"选择文件夹"后,请在系统文件管理器中找到并选中上述文件夹。'),
],
),
actions: [
TextButton(
onPressed: () {
Navigator.pop(context);
// 用户取消后,保持当前状态,但不阻塞后续操作
setState(() {
_safState = SafPermissionState.denied;
});
},
child: const Text('取消'),
),
ElevatedButton(
onPressed: () async {
final ctx = context; // 保存引用
Navigator.pop(ctx);
final result = await _safService.requestPermission();
if (result.success) {
if (!mounted) return; // 先检查 State 是否还在
setState(() {
_safState = SafPermissionState.authorized;
});
readBatteryCapacity();
} else {
if (!mounted) return;
ScaffoldMessenger.of(ctx).showSnackBar(
SnackBar(
content: Text(result.errorMessage ?? '授权失败,请重试'),
backgroundColor: Colors.red,
),
);
// 重新弹窗
_showSafAuthorizationDialog();
}
},
),
],
),
);
}
// ---------- 核心:系统API定调 ---------- // ---------- 核心:系统API定调 ----------
void _applySystemChargeState(bool isCharging, String systemChargeType) { void _applySystemChargeState(bool isCharging, String systemChargeType) {
dev.log( dev.log(
@@ -263,12 +331,11 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
_chargeVolt = 0.0; _chargeVolt = 0.0;
_chargeCurr = 0.0; _chargeCurr = 0.0;
_wiredLevel = null; _wiredLevel = null;
_chargingStartTime = null; // 🟢 清空开始时间 _chargingStartTime = null;
_updateChargeState(); _updateChargeState();
return; return;
} }
// 🟢 1.3.0-dev.3: 记录充电开始时刻(仅当从断开状态变为充电状态时)
if (_chargingStartTime == null) { if (_chargingStartTime == null) {
_chargingStartTime = DateTime.now().toUtc(); _chargingStartTime = DateTime.now().toUtc();
dev.log( dev.log(
@@ -297,7 +364,6 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
_isWireless = false; _isWireless = false;
_chargeType = "SDP"; _chargeType = "SDP";
} else { } else {
// UNKNOWN 时清空旧类型,等待日志修正
dev.log( dev.log(
'[_applySystemChargeState] ⚠️ 未知类型: $systemChargeType,清空类型等待日志', '[_applySystemChargeState] ⚠️ 未知类型: $systemChargeType,清空类型等待日志',
name: 'BatteryHealth', name: 'BatteryHealth',
@@ -324,7 +390,6 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
name: 'BatteryHealth', name: 'BatteryHealth',
); );
// ---------- 1. 收集所有在有效时间窗口内的数据行 ----------
List<Map<String, dynamic>> validLines = []; List<Map<String, dynamic>> validLines = [];
for (String line in lines) { for (String line in lines) {
DateTime? timestamp = _parseLogTimestamp(line); DateTime? timestamp = _parseLogTimestamp(line);
@@ -349,10 +414,8 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
return; return;
} }
// 按时间戳排序(从新到旧)
validLines.sort((a, b) => b['timestamp'].compareTo(a['timestamp'])); validLines.sort((a, b) => b['timestamp'].compareTo(a['timestamp']));
// 取最新的一条
Map<String, dynamic> latest = validLines.first; Map<String, dynamic> latest = validLines.first;
String latestLine = latest['line']; String latestLine = latest['line'];
String latestType = latest['type']; String latestType = latest['type'];
@@ -363,7 +426,7 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
name: 'BatteryHealth', name: 'BatteryHealth',
); );
// ---------- 2. 无线处理 ---------- // ---------- 无线 ----------
if (latestType == 'wls') { if (latestType == 'wls') {
dev.log('[_parseChargeDataFromLog] 处理无线数据', name: 'BatteryHealth'); dev.log('[_parseChargeDataFromLog] 处理无线数据', name: 'BatteryHealth');
@@ -390,7 +453,7 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
return; return;
} }
// ---------- 3. 有线处理 ---------- // ---------- 有线 ----------
if (latestType == 'usb') { if (latestType == 'usb') {
dev.log('[_parseChargeDataFromLog] 处理有线数据', name: 'BatteryHealth'); dev.log('[_parseChargeDataFromLog] 处理有线数据', name: 'BatteryHealth');
@@ -411,7 +474,6 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
_isWireless = false; _isWireless = false;
// 🟢 1.3.0-dev.2: real_type 优先级最高
if (realType != null && realType == "PPS") { if (realType != null && realType == "PPS") {
_chargeType = "PPS"; _chargeType = "PPS";
} else if (realType != null && } else if (realType != null &&
@@ -438,14 +500,13 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
} }
} }
// 🟢 1.3.0-dev.3: 增加超时保护 // ---------- SAF 文件读取 ----------
Future<void> _fetchChargeDataFromLog() async { Future<void> _fetchChargeDataFromLog() async {
if (!_isCharging) { if (!_isCharging) {
dev.log('[_fetchChargeDataFromLog] 未充电,跳过读取', name: 'BatteryHealth'); dev.log('[_fetchChargeDataFromLog] 未充电,跳过读取', name: 'BatteryHealth');
return; return;
} }
// 超时检查:如果插入超过 5 分钟还没读到数据,不再频繁报错
if (_chargingStartTime != null) { if (_chargingStartTime != null) {
Duration elapsed = DateTime.now().toUtc().difference(_chargingStartTime!); Duration elapsed = DateTime.now().toUtc().difference(_chargingStartTime!);
if (elapsed.inMinutes > 5) { if (elapsed.inMinutes > 5) {
@@ -457,24 +518,35 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
} }
} }
String fullPath = _filePath + getTodayFileName(); // 检查 SAF 授权
dev.log('[_fetchChargeDataFromLog] 尝试读取: $fullPath', name: 'BatteryHealth'); _safState = await _safService.getPermissionState();
if (_safState != SafPermissionState.authorized) {
if (mounted) _showSafAuthorizationDialog();
return;
}
File file = File(fullPath); final fileName = getTodayFileName();
if (!await file.exists()) { dev.log('[_fetchChargeDataFromLog] 尝试读取: $fileName', name: 'BatteryHealth');
final exists = await _safService.logFileExists(fileName);
if (!exists) {
dev.log( dev.log(
'[_fetchChargeDataFromLog] ⚠️ 文件不存在: $fullPath', '[_fetchChargeDataFromLog] ⚠️ 文件不存在: $fileName',
name: 'BatteryHealth', name: 'BatteryHealth',
); );
return; return;
} }
try { try {
List<String> lines = await file.readAsLines(); final lines = await _safService.readLogFile(fileName);
if (lines.isEmpty) {
dev.log('[_fetchChargeDataFromLog] 文件为空', name: 'BatteryHealth');
return;
}
dev.log( dev.log(
'[_fetchChargeDataFromLog] ✅ 读取成功,共 ${lines.length}', '[_fetchChargeDataFromLog] ✅ 读取成功,共 ${lines.length}',
name: 'BatteryHealth', name: 'BatteryHealth',
); );
if (lines.isEmpty) return;
_parseChargeDataFromLog(lines); _parseChargeDataFromLog(lines);
} catch (e) { } catch (e) {
dev.log('[_fetchChargeDataFromLog] ❌ 读取异常: $e', name: 'BatteryHealth'); dev.log('[_fetchChargeDataFromLog] ❌ 读取异常: $e', name: 'BatteryHealth');
@@ -535,19 +607,20 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
// ---------- 读取当前数据 ---------- // ---------- 读取当前数据 ----------
Future<void> readBatteryCapacity() async { Future<void> readBatteryCapacity() async {
var status = await Permission.manageExternalStorage.request(); // 检查 SAF 授权
if (!status.isGranted) { _safState = await _safService.getPermissionState();
setState(() => _hasData = false); if (_safState != SafPermissionState.authorized) {
if (mounted) _showSafAuthorizationDialog();
return; return;
} }
await _getDeviceInfo(); await _getDeviceInfo();
await _scanAndUpdateDB(); await _scanAndUpdateDB();
String fullPath = _filePath + getTodayFileName(); final fileName = getTodayFileName();
File file = File(fullPath); final exists = await _safService.logFileExists(fileName);
if (!await file.exists()) { if (!exists) {
List<Map<String, dynamic>> recent = await _queryRecentData(1); List<Map<String, dynamic>> recent = await _queryRecentData(1);
if (recent.isNotEmpty) { if (recent.isNotEmpty) {
setState(() { setState(() {
@@ -567,7 +640,7 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
} }
try { try {
List<String> lines = await file.readAsLines(); final lines = await _safService.readLogFile(fileName);
if (lines.isEmpty) { if (lines.isEmpty) {
setState(() => _hasData = false); setState(() => _hasData = false);
if (_isCharging) _fetchChargeDataFromLog(); if (_isCharging) _fetchChargeDataFromLog();
@@ -700,8 +773,16 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
} }
}); });
WidgetsBinding.instance.addPostFrameCallback((_) { // 延迟初始化 SAF 并读取数据
WidgetsBinding.instance.addPostFrameCallback((_) async {
await _safService.init();
_safState = await _safService.getPermissionState();
if (_safState == SafPermissionState.unauthorized ||
_safState == SafPermissionState.expired) {
if (mounted) _showSafAuthorizationDialog();
} else {
readBatteryCapacity(); readBatteryCapacity();
}
}); });
} }
@@ -799,9 +880,7 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
fontSize: 40, fontSize: 40,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
height: 1.0, height: 1.0,
color: healthPercent < 70 color: healthPercent < 70 ? Colors.red : Colors.black,
? Colors.red
: Colors.black, // 🟢 动态颜色
), ),
), ),
const SizedBox(width: 2), const SizedBox(width: 2),
@@ -813,7 +892,7 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
height: 1.0, height: 1.0,
color: healthPercent < 70 color: healthPercent < 70
? Colors.red ? Colors.red
: Colors.grey.shade600, // 🟢 动态颜色 : Colors.grey.shade600,
), ),
), ),
], ],
@@ -1047,7 +1126,7 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
} else if (value >= 70) { } else if (value >= 70) {
barColor = kHealthYellow; barColor = kHealthYellow;
} else { } else {
barColor = Colors.red; // 🟢 小于 70 显示红色 barColor = Colors.red;
} }
} else { } else {
barColor = Colors.grey.shade300; barColor = Colors.grey.shade300;
+171
View File
@@ -0,0 +1,171 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter_saf/flutter_saf.dart';
import '../models/saf_permission_state.dart';
/// SAF 存储服务
/// 负责管理通过 SAF 授权的文件夹访问
class SafStorageService {
static final SafStorageService _instance = SafStorageService._internal();
factory SafStorageService() => _instance;
SafStorageService._internal();
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) {
_clearCache();
return SafPermissionState.expired;
}
return SafPermissionState.authorized;
} catch (e) {
_clearCache();
return SafPermissionState.expired;
}
}
/// 请求 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);
} catch (e) {
if (kDebugMode) {
print('[SAF] 授权失败: $e');
}
return SafAuthorizationResult.error('授权失败: $e');
}
}
/// 检查指定日期的日志文件是否存在
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);
} catch (e) {
return false;
}
}
/// 读取日志文件内容
/// 返回文件行列表,如果文件不存在或读取失败返回空列表
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');
} catch (e) {
if (kDebugMode) {
print('[SAF] 读取文件失败: $e');
}
return [];
}
}
/// 获取文件夹中的文件列表
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) {
return [];
}
}
/// 检查授权是否有效,如果失效则清除缓存
Future<bool> validatePermission() async {
final state = await getPermissionState();
return state == SafPermissionState.authorized;
}
/// 清除授权缓存
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;
}
@@ -7,10 +7,12 @@ import Foundation
import device_info_plus import device_info_plus
import path_provider_foundation import path_provider_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"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
} }
+56
View File
@@ -336,6 +336,62 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.8" version: "2.1.8"
shared_preferences:
dependency: "direct main"
description:
name: shared_preferences
sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf
url: "https://pub.dev"
source: hosted
version: "2.5.5"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53
url: "https://pub.dev"
source: hosted
version: "2.4.23"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f"
url: "https://pub.dev"
source: hosted
version: "2.5.6"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shared_preferences_platform_interface:
dependency: transitive
description:
name: shared_preferences_platform_interface
sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9"
url: "https://pub.dev"
source: hosted
version: "2.4.2"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019
url: "https://pub.dev"
source: hosted
version: "2.4.3"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
sky_engine: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter
+2
View File
@@ -37,6 +37,8 @@ dependencies:
path_provider: ^2.1.0 path_provider: ^2.1.0
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
flutter_saf: ^0.1.0
# 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.