新增阅读器功能,待优化

This commit is contained in:
DelLevin-Home
2026-06-20 00:54:40 +08:00
parent 0af9d4368a
commit 670502b4f5
250 changed files with 97847 additions and 826 deletions

View File

@@ -0,0 +1,75 @@
import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:path/path.dart' as p;
import 'package:uuid/uuid.dart';
import '../models/reader_book.dart';
import '../providers/app_provider.dart';
import '../utils/book_file_helper.dart';
/// 书籍导入服务
class BookImportService {
static const allowedExtensions = ['epub', 'txt'];
static Future<ReaderBook?> pickAndImportBook(
BuildContext context,
AppProvider provider,
) async {
final result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: allowedExtensions,
allowMultiple: false,
);
if (result == null || result.files.isEmpty) return null;
final platformFile = result.files.first;
final sourcePath = platformFile.path;
if (sourcePath == null) return null;
final file = File(sourcePath);
if (!await file.exists()) return null;
final extension = p.extension(file.path).replaceAll('.', '').toLowerCase();
if (!allowedExtensions.contains(extension)) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('不支持的格式:$extension')),
);
}
return null;
}
final title = p.basenameWithoutExtension(platformFile.name);
final helper = BookFileHelper.instance;
final id = const Uuid().v4();
// 清理文件名,去除特殊字符
final safeName = platformFile.name.replaceAll(RegExp(r'[<>:"/\\|?*]'), '_');
final destPath = await helper.bookFile(id, safeName);
// 复制文件
await file.copy(destPath);
final now = DateTime.now();
final readerBook = ReaderBook(
id: id,
title: title,
fileName: platformFile.name,
filePath: '$id/$safeName', // 相对路径
fileExtension: extension,
createdAt: now,
updatedAt: now,
);
await provider.addReaderBook(readerBook);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('$title」导入成功')),
);
}
return readerBook;
}
}

View File

@@ -0,0 +1,98 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:shelf/shelf.dart' as shelf;
import 'package:shelf/shelf_io.dart' as io;
/// 本地 HTTP 服务器,为 WebView 提供书籍文件和 foliate-js 资源
class Server {
static final Server _singleton = Server._internal();
factory Server() => _singleton;
Server._internal();
HttpServer? _server;
bool get isRunning => _server != null;
int get port => _server?.port ?? 0;
Future<void> start({int preferredPort = 0}) async {
if (_server != null) {
await stop();
}
final handler = const shelf.Pipeline()
.addMiddleware(shelf.logRequests())
.addHandler(_handleRequest);
try {
_server = await io.serve(handler, '127.0.0.1', preferredPort);
} catch (_) {
_server = await io.serve(handler, '127.0.0.1', 0);
}
}
Future<void> stop() async {
if (_server == null) return;
await _server!.close(force: true);
_server = null;
}
Future<shelf.Response> _handleRequest(shelf.Request request) async {
final uriPath = request.requestedUri.path;
// 书籍文件请求
if (uriPath.startsWith('/book/')) {
final bookPath = Uri.decodeComponent(uriPath.substring(6));
final file = File(bookPath);
if (!await file.exists()) {
return shelf.Response.notFound('Book not found');
}
return shelf.Response.ok(
file.openRead(),
headers: {
'Content-Type': 'application/epub+zip',
'Access-Control-Allow-Origin': '*',
},
);
}
// foliate-js 资源请求
if (uriPath.startsWith('/foliate-js/')) {
final assetPath = 'assets/foliate-js/${uriPath.substring(12)}';
String contentType;
if (uriPath.endsWith('.html')) {
contentType = 'text/html';
} else if (uriPath.endsWith('.css')) {
contentType = 'text/css';
} else if (uriPath.endsWith('.js') || uriPath.endsWith('.mjs')) {
contentType = 'application/javascript';
} else if (uriPath.endsWith('.json')) {
contentType = 'application/json';
} else if (uriPath.endsWith('.svg')) {
contentType = 'image/svg+xml';
} else {
contentType = 'application/octet-stream';
}
try {
// 优先尝试 load() 加载为字节流(最可靠),再转为字符串或直接返回
final data = await rootBundle.load(assetPath);
return shelf.Response.ok(
data.buffer.asUint8List(),
headers: {
'Content-Type': contentType,
'Access-Control-Allow-Origin': '*',
},
);
} catch (e) {
debugPrint('[Server] Asset not found: $assetPath error=$e');
return shelf.Response.notFound('Asset not found: $assetPath');
}
}
return shelf.Response.ok(
'OK',
headers: {'Access-Control-Allow-Origin': '*'},
);
}
}