diff --git a/lib/pages/battery_health_page.dart b/lib/pages/battery_health_page.dart index e63d7e5..e1b0628 100644 --- a/lib/pages/battery_health_page.dart +++ b/lib/pages/battery_health_page.dart @@ -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 // ---------- SAF 授权引导 ---------- void _showSafAuthorizationDialog() { +<<<<<<< HEAD +======= + // 避免重复弹窗 +>>>>>>> c5a85e05a3bb50cd4ea7de0da2db94c867eb7ddd if (!mounted) return; showDialog( @@ -275,6 +280,10 @@ class _BatteryHealthPageState extends State TextButton( onPressed: () { Navigator.pop(context); +<<<<<<< HEAD +======= + // 用户取消后,保持当前状态,但不阻塞后续操作 +>>>>>>> c5a85e05a3bb50cd4ea7de0da2db94c867eb7ddd setState(() { _safState = SafPermissionState.denied; }); @@ -283,11 +292,19 @@ class _BatteryHealthPageState extends State ), 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 backgroundColor: Colors.red, ), ); +<<<<<<< HEAD _showSafAuthorizationDialog(); } }, child: const Text('选择文件夹'), // 🟢 加这行 +======= + // 重新弹窗 + _showSafAuthorizationDialog(); + } + }, +>>>>>>> c5a85e05a3bb50cd4ea7de0da2db94c867eb7ddd ), ], ), diff --git a/lib/services/saf_storage_service.dart b/lib/services/saf_storage_service.dart index a109bac..0d763ac 100644 --- a/lib/services/saf_storage_service.dart +++ b/lib/services/saf_storage_service.dart @@ -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 init() async { + final prefs = await SharedPreferences.getInstance(); + _cachedUriString = prefs.getString(_prefKeySafUri); + if (kDebugMode) { + print('[SAF] 初始化,缓存 URI: $_cachedUriString'); + } + } + + /// 获取当前授权状态 + Future 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 requestPermission() async { try { // 使用 file_picker 选择文件夹 @@ -60,6 +98,40 @@ class SafStorageService { } return SafAuthorizationResult.success(selectedPath); +======= + /// 请求 SAF 授权(引导用户选择文件夹) + /// 返回授权结果 + Future 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 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 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> 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> 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> listFiles() async { final state = await getPermissionState(); if (state != SafPermissionState.authorized) return []; @@ -109,15 +216,34 @@ class SafStorageService { if (kDebugMode) { print('[SAF] 列出文件失败: $e'); } +======= + /// 获取文件夹中的文件列表 + Future> 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 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 _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 } diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 852819c..649d2d5 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -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")) } diff --git a/pubspec.lock b/pubspec.lock index a51ba58..247e167 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -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: diff --git a/pubspec.yaml b/pubspec.yaml index 5aec786..c8ac953 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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.