新增阅读器功能,待优化

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,71 @@
import 'dart:io';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as p;
/// 阅读器文件路径管理
class BookFileHelper {
static final BookFileHelper instance = BookFileHelper._init();
BookFileHelper._init();
String? _rootPath;
Future<String> get _root async {
if (_rootPath != null) return _rootPath!;
final dir = await getApplicationDocumentsDirectory();
_rootPath = p.join(dir.path, 'mooknote', 'book_file');
await Directory(_rootPath!).create(recursive: true);
return _rootPath!;
}
Future<String> get bookFileRoot async => _root;
Future<String> get coverDir async {
final root = await _root;
final dir = p.join(root, 'cover');
await Directory(dir).create(recursive: true);
return dir;
}
Future<String> bookDir(String bookId) async {
final root = await _root;
final dir = p.join(root, bookId);
await Directory(dir).create(recursive: true);
return dir;
}
Future<String> bookFile(String bookId, String fileName) async {
final dir = await bookDir(bookId);
return p.join(dir, fileName);
}
String? relativePath(String absolutePath) {
if (_rootPath == null) return null;
if (absolutePath.startsWith(_rootPath!)) {
return absolutePath.substring(_rootPath!.length + 1);
}
return null;
}
Future<String> absolutePath(String relativePath) async {
final root = await _root;
return p.join(root, relativePath);
}
Future<void> deleteBookFiles(String bookId) async {
final dir = await bookDir(bookId);
if (await Directory(dir).exists()) {
await Directory(dir).delete(recursive: true);
}
}
/// 同步初始化(必须在使用 resolveAbsolutePath 前调用一次 bookFileRoot
Future<void> ensureInitialized() async {
await _root;
}
/// 根据相对路径解析绝对路径(调用前需确保已初始化)
String resolveAbsolutePath(String relativePath) {
if (_rootPath == null) return relativePath;
return p.join(_rootPath!, relativePath);
}
}

View File

@@ -0,0 +1,6 @@
/// Dart ARGB 颜色字符串转换为 JS RGBA 格式
/// dartColor: "FF0066FF" (AARRGGBB) → jsColor: "0066FFFF" (RRGGBBAA)
String convertDartColorToJs(String dartColor) {
if (dartColor.length < 8) return dartColor;
return dartColor.substring(2) + dartColor.substring(0, 2);
}

View File

@@ -57,7 +57,7 @@ class DatabaseHelper {
return await openDatabase(
path,
version: 13,
version: 14,
onCreate: _createDB,
onUpgrade: _onUpgrade,
);
@@ -115,6 +115,9 @@ class DatabaseHelper {
if (oldVersion < 13) {
await _upgradeToV13(db);
}
if (oldVersion < 14) {
await _createReaderBooksTable(db);
}
}
/// 升级books表到V11添加ISBN和出版时间字段
@@ -154,6 +157,26 @@ class DatabaseHelper {
}
}
/// 升级到V14创建阅读器书籍表
Future<void> _createReaderBooksTable(Database db) async {
await db.execute('''
CREATE TABLE IF NOT EXISTS reader_books (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
author TEXT DEFAULT '',
cover_path TEXT,
file_path TEXT NOT NULL,
file_name TEXT NOT NULL,
file_extension TEXT NOT NULL DEFAULT 'epub',
last_read_cfi TEXT DEFAULT '',
reading_percentage REAL DEFAULT 0.0,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
is_deleted INTEGER DEFAULT 0
)
''');
}
/// 升级到V13创建标签表并回填已有数据
Future<void> _upgradeToV13(Database db) async {
await db.execute('''
@@ -587,6 +610,24 @@ class DatabaseHelper {
UNIQUE(name, type)
)
''');
// 阅读器书籍表
await db.execute('''
CREATE TABLE IF NOT EXISTS reader_books (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
author TEXT DEFAULT '',
cover_path TEXT,
file_path TEXT NOT NULL,
file_name TEXT NOT NULL,
file_extension TEXT NOT NULL DEFAULT 'epub',
last_read_cfi TEXT DEFAULT '',
reading_percentage REAL DEFAULT 0.0,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
is_deleted INTEGER DEFAULT 0
)
''');
}
// 关闭数据库

View File

@@ -0,0 +1,69 @@
import 'package:flutter/foundation.dart';
import '../models/reader_book.dart';
import 'database_helper.dart';
/// 阅读器书籍 DAO
class ReaderBookDao {
final DatabaseHelper _dbHelper = DatabaseHelper.instance;
Future<T> _wrap<T>(String op, Future<T> Function() fn) async {
try {
return await fn();
} catch (e) {
debugPrint('[ReaderBookDao] $op error: $e');
rethrow;
}
}
Future<List<ReaderBook>> getAllReaderBooks() => _wrap('getAllReaderBooks', () async {
final db = await _dbHelper.database;
final List<Map<String, dynamic>> maps = await db.query(
'reader_books',
where: 'is_deleted = ?',
whereArgs: [0],
orderBy: 'created_at DESC',
);
return List.generate(maps.length, (i) => ReaderBook.fromJson(maps[i]));
});
Future<ReaderBook?> getReaderBookById(String id) => _wrap('getReaderBookById', () async {
final db = await _dbHelper.database;
final List<Map<String, dynamic>> maps = await db.query(
'reader_books',
where: 'id = ? AND is_deleted = ?',
whereArgs: [id, 0],
);
if (maps.isEmpty) return null;
return ReaderBook.fromJson(maps.first);
});
Future<void> insertReaderBook(ReaderBook book) => _wrap('insertReaderBook', () async {
final db = await _dbHelper.database;
await db.insert('reader_books', book.toJson());
});
Future<void> updateReaderBook(ReaderBook book) => _wrap('updateReaderBook', () async {
final db = await _dbHelper.database;
await db.update(
'reader_books',
book.toJson(),
where: 'id = ?',
whereArgs: [book.id],
);
});
Future<void> deleteReaderBook(String id) => _wrap('deleteReaderBook', () async {
final db = await _dbHelper.database;
await db.update(
'reader_books',
{'is_deleted': 1, 'updated_at': DateTime.now().toIso8601String()},
where: 'id = ?',
whereArgs: [id],
);
});
Future<void> permanentDeleteReaderBook(String id) => _wrap('permanentDeleteReaderBook', () async {
final db = await _dbHelper.database;
await db.delete('reader_books', where: 'id = ?', whereArgs: [id]);
});
}

View File

@@ -0,0 +1,69 @@
import 'dart:convert';
import '../service/book_server.dart';
import 'color_converter.dart';
/// 生成 foliate-js 阅读器 URL
String generateReaderUrl({
required String fileUrl,
String cfi = '',
required String backgroundColor,
required String textColor,
bool isDarkMode = false,
}) {
final indexHtmlPath = 'http://127.0.0.1:${Server().port}/foliate-js/index.html';
final jsBg = convertDartColorToJs(backgroundColor);
final jsTc = convertDartColorToJs(textColor);
final style = {
'fontSize': 100, // 100 = base 100%
'fontName': '',
'fontPath': '',
'fontWeight': 400,
'letterSpacing': 0,
'spacing': 1.6,
'paragraphSpacing': 0.6,
'textIndent': 2,
'fontColor': '#$jsTc',
'backgroundColor': '#$jsBg',
'topMargin': 25,
'bottomMargin': 25,
'sideMargin': 15,
'justify': true,
'hyphenate': false,
'pageTurnStyle': 'slide',
'maxColumnCount': 1,
'columnThreshold': 3,
'writingMode': 'horizontal-tb',
'textAlign': 'justify',
'backgroundImage': '',
'bgimgBlur': 0,
'bgimgOpacity': 1.0,
'bgimgFit': 'cover',
'allowScript': false,
'customCSS': '',
'customCSSEnabled': false,
'useBookStyles': true,
'headingFontSize': 130,
'codeHighlightTheme': 'atom-one-light',
};
final readingRules = {
'convertChineseMode': 'none',
'bionicReadingMode': false,
};
final params = {
'importing': false,
'url': fileUrl,
'initialCfi': cfi,
'style': style,
'readingRules': readingRules,
};
final queryParts = params.entries
.map((e) => '${e.key}=${Uri.encodeComponent(jsonEncode(e.value))}')
.join('&');
return '$indexHtmlPath?$queryParts';
}