generated from dellevin/template
新增阅读器功能,待优化
This commit is contained in:
75
lib/service/book_import_service.dart
Normal file
75
lib/service/book_import_service.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
98
lib/service/book_server.dart
Normal file
98
lib/service/book_server.dart
Normal 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': '*'},
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user