fix android

This commit is contained in:
2025-07-24 02:59:58 +08:00
parent 53171e900d
commit 768bd4b71b
5 changed files with 77 additions and 58 deletions

View File

@@ -1,10 +1,13 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<application
android:label="灵猫小说下载器"
android:name="${applicationName}"
android:requestLegacyExternalStorage="true"
android:icon="@mipmap/launcher_icon">
<activity
android:name=".MainActivity"

View File

@@ -1,14 +1,14 @@
// lib/ui/widgets/book_detail_view.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:file_picker/file_picker.dart';
import 'package:open_file/open_file.dart';
import 'package:flutter/foundation.dart';
import 'dart:io' show Platform , File, Directory;
import 'dart:io' show Platform , Directory;
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:file_saver/file_saver.dart';
import 'package:device_info_plus/device_info_plus.dart'; // 新增: 用于获取设备信息
import '../../core/book_downloader.dart';
import '../../models/book.dart';
@@ -17,23 +17,6 @@ import '../../providers/book_provider.dart';
final bool isAndroid = !kIsWeb && Platform.isAndroid;
final bool isIOS = !kIsWeb && Platform.isIOS;
/// (仅安卓) 检查文件路径是否存在,如果存在,则返回一个新的、不冲突的文件路径。
Future<String> _getUniqueFilePath(String filePath) async {
String newPath = filePath;
int counter = 1;
final context = p.Context(style: p.Style.posix);
while (await File(newPath).exists() || await Directory(newPath).exists()) {
final String directory = context.dirname(filePath);
final String extension = context.extension(filePath);
final String filenameWithoutExt = context.basenameWithoutExtension(filePath);
newPath = context.join(directory, '$filenameWithoutExt($counter)$extension');
counter++;
}
return newPath;
}
class BookDetailView extends ConsumerStatefulWidget {
const BookDetailView({super.key});
@@ -44,19 +27,15 @@ class BookDetailView extends ConsumerStatefulWidget {
class _BookDetailViewState extends ConsumerState<BookDetailView> {
DownloadFormat _selectedFormat = DownloadFormat.singleTxt;
// --- 新增: 状态变量,用于在异步方法和 build 方法之间传递数据 ---
String? _lastDownloadedPath;
Future<String?> _getMobileDownloadsDirectory() async {
Directory? directory;
try {
if (Platform.isIOS) {
// iOS 平台保存到应用文档目录
directory = await getApplicationDocumentsDirectory();
} else {
// Android 平台,首先尝试公共的 Download 目录
directory = Directory('/storage/emulated/0/Download');
// 如果这个目录因为某些原因不存在,则回退到应用外部存储的根目录
if (!await directory.exists()) {
directory = await getExternalStorageDirectory();
}
@@ -75,7 +54,6 @@ class _BookDetailViewState extends ConsumerState<BookDetailView> {
try {
if (kIsWeb) {
// --- Web 平台逻辑 ---
if (_selectedFormat == DownloadFormat.chapterTxt) {
showDialog(
context: context,
@@ -92,44 +70,59 @@ class _BookDetailViewState extends ConsumerState<BookDetailView> {
);
return;
} else {
// 对于 Web我们不需要真实的目录。我们只用文件名作为标识。
// 下载器将会在内存中处理数据,而不是写入文件。
String extension = _selectedFormat == DownloadFormat.singleTxt ? 'txt' : 'epub';
outputPath = '$fileName.$extension';
}
} else if (isAndroid || isIOS) {
// --- 移动平台 (Android/iOS) 逻辑 ---
var hasPermission = true;
if (isAndroid) {
// 只在 Android 上请求权限
var status = await Permission.storage.status;
if (status.isDenied) {
status = await Permission.storage.request();
// --- 修改点: 根据安卓版本请求权限 ---
final deviceInfo = await DeviceInfoPlugin().androidInfo;
PermissionStatus status;
// Android 11 (API 30) 或更高版本
if (deviceInfo.version.sdkInt >= 30) {
status = await Permission.manageExternalStorage.status;
if (!status.isGranted) {
status = await Permission.manageExternalStorage.request();
}
} else { // Android 10 (API 29) 或更低版本
status = await Permission.storage.status;
if (!status.isGranted) {
status = await Permission.storage.request();
}
}
hasPermission = status.isGranted;
}
if (hasPermission) {
// --- 核心修改点: 使用新的路径获取方法 ---
final String? downloadsPath = await _getMobileDownloadsDirectory();
if (downloadsPath != null) {
String initialPath;
if (_selectedFormat == DownloadFormat.chapterTxt) {
initialPath = '$downloadsPath/$fileName';
outputPath = '$downloadsPath/$fileName';
} else {
String extension = _selectedFormat == DownloadFormat.singleTxt ? 'txt' : 'epub';
initialPath = '$downloadsPath/$fileName.$extension';
outputPath = '$downloadsPath/$fileName.$extension';
}
// 同样只在 Android 上处理同名问题
outputPath = isAndroid ? await _getUniqueFilePath(initialPath) : initialPath;
// --- 已移除: 不再调用 _getUniqueFilePath ---
} else {
throw Exception("无法获取下载目录。");
}
} else {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('需要存储权限才能下载文件。')),
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('权限不足'),
content: const Text('需要存储权限才能下载文件'),
actions: [
TextButton(
child: const Text('确定'),
onPressed: () => Navigator.of(context).pop(),
),
],
),
);
return;
}
@@ -153,11 +146,9 @@ class _BookDetailViewState extends ConsumerState<BookDetailView> {
}
if (outputPath != null) {
// --- 修改点: 在启动下载前,保存文件路径到状态变量 ---
setState(() {
_lastDownloadedPath = outputPath;
});
// --- 修改点: 移除了这里的 ref.listen ---
ref.read(downloadProvider.notifier).startDownload(
book: book,
format: _selectedFormat,
@@ -173,31 +164,23 @@ class _BookDetailViewState extends ConsumerState<BookDetailView> {
@override
Widget build(BuildContext context) {
// --- 核心修改点: 调整 ref.listen 的内部逻辑 ---
ref.listen<DownloadState>(downloadProvider, (previous, next) {
if (previous?.isDownloading == true && !next.isDownloading && next.status.contains('成功')) {
// 检查路径是否存在
if (_lastDownloadedPath != null) {
// --- 解决方案: 创建一个局部 final 变量来捕获当前路径的值 ---
final String path = _lastDownloadedPath!;
final String fileName = p.basename(path); // 从完整路径中提取文件名
final String fileName = p.basename(path);
// 显示 SnackBar
if (kIsWeb) {
// --- Web 平台: 直接从 state 获取 bytes 并触发浏览器下载 ---
if (next.data != null) {
FileSaver.instance.saveFile(
name: p.basenameWithoutExtension(fileName), // 文件名 (无扩展名)
bytes: next.data!, // 文件内容
fileExtension: p.extension(fileName).replaceFirst('.', ''), // 扩展名 (去掉点)
mimeType: MimeType.text
name: p.basenameWithoutExtension(fileName),
bytes: next.data!,
fileExtension: p.extension(fileName).replaceFirst('.', ''),
mimeType: MimeType.text
);
// --- 新增: 保存文件后,调用清理方法 ---
ref.read(downloadProvider.notifier).clearDownloadData();
}
} else {
// --- 移动/桌面平台: 显示带“打开”按钮的 SnackBar ---
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('下载完成: $path'),
@@ -210,7 +193,6 @@ class _BookDetailViewState extends ConsumerState<BookDetailView> {
)
);
}
// 现在可以安全地清空成员变量,为下一次下载做准备了
_lastDownloadedPath = null;
}
}
@@ -301,7 +283,6 @@ class _BookDetailViewState extends ConsumerState<BookDetailView> {
);
}
// _buildDownloadControls 方法保持不变
Widget _buildDownloadControls(Book book) {
final downloadState = ref.watch(downloadProvider);
@@ -385,4 +366,4 @@ class _BookDetailViewState extends ConsumerState<BookDetailView> {
},
);
}
}
}

View File

@@ -5,6 +5,7 @@
import FlutterMacOS
import Foundation
import device_info_plus
import file_picker
import file_saver
import open_file_mac
@@ -14,6 +15,7 @@ import url_launcher_macos
import window_size
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
FileSaverPlugin.register(with: registry.registrar(forPlugin: "FileSaverPlugin"))
OpenFilePlugin.register(with: registry.registrar(forPlugin: "OpenFilePlugin"))

View File

@@ -121,6 +121,22 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.8"
device_info_plus:
dependency: "direct main"
description:
name: device_info_plus
sha256: "98f28b42168cc509abc92f88518882fd58061ea372d7999aecc424345c7bff6a"
url: "https://pub.flutter-io.cn"
source: hosted
version: "11.5.0"
device_info_plus_platform_interface:
dependency: transitive
description:
name: device_info_plus_platform_interface
sha256: e1ea89119e34903dca74b883d0dd78eb762814f97fb6c76f35e9ff74d261a18f
url: "https://pub.flutter-io.cn"
source: hosted
version: "7.0.3"
dio:
dependency: "direct main"
description:
@@ -161,6 +177,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.4"
file:
dependency: transitive
description:
name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.flutter-io.cn"
source: hosted
version: "7.0.1"
file_picker:
dependency: "direct main"
description:
@@ -738,6 +762,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "5.14.0"
win32_registry:
dependency: transitive
description:
name: win32_registry
sha256: "6f1b564492d0147b330dd794fee8f512cec4977957f310f9951b5f9d83618dae"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.0"
window_size:
dependency: "direct main"
description:
@@ -772,4 +804,4 @@ packages:
version: "3.1.3"
sdks:
dart: ">=3.8.1 <4.0.0"
flutter: ">=3.27.0"
flutter: ">=3.29.0"

View File

@@ -58,6 +58,7 @@ dependencies:
window_size: ^0.1.0
url_launcher: ^6.0.0
package_info_plus: ^8.3.0
device_info_plus: ^11.5.0
dev_dependencies:
flutter_test: