基础epub阅读功能

This commit is contained in:
DelLevin-Home
2026-06-27 11:20:27 +08:00
parent 02242089e8
commit 5acbc3a602
18 changed files with 980 additions and 330 deletions

View File

@@ -39,7 +39,7 @@ class DatabaseHelper {
return await openDatabase(
path,
version: 22,
version: 23,
onCreate: _createDB,
onUpgrade: _onUpgrade,
);
@@ -156,6 +156,10 @@ class DatabaseHelper {
// 为笔记表添加置顶字段
await _upgradeNotesTableV22(db);
}
if (oldVersion < 23) {
// 创建书籍批注表(高亮、下划线、书签)
await _createBookAnnotationsTable(db);
}
}
/// 升级books表到V11添加ISBN和出版时间字段
@@ -683,6 +687,9 @@ class DatabaseHelper {
// Note Plus 块编辑器文档表
await _createNotePlusTable(db);
// 书籍批注表
await _createBookAnnotationsTable(db);
}
/// 创建 Note Plus 文档表
@@ -703,6 +710,32 @@ class DatabaseHelper {
''');
}
/// 创建书籍批注表(高亮、下划线、书签)
Future<void> _createBookAnnotationsTable(Database db) async {
await db.execute('''
CREATE TABLE IF NOT EXISTS book_annotations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
book_id TEXT NOT NULL,
content TEXT NOT NULL DEFAULT '',
cfi TEXT NOT NULL DEFAULT '',
chapter TEXT DEFAULT '',
type TEXT NOT NULL DEFAULT 'highlight',
color TEXT NOT NULL DEFAULT 'FFEB3B',
reader_note TEXT DEFAULT '',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
)
''');
// 索引:按 book_id 查询加速
await db.execute(
'CREATE INDEX IF NOT EXISTS idx_book_annotations_book_id ON book_annotations(book_id)',
);
// 索引:按 book_id + type 查询加速
await db.execute(
'CREATE INDEX IF NOT EXISTS idx_book_annotations_type ON book_annotations(book_id, type)',
);
}
// 关闭数据库
Future close() async {
if (_database != null) {

View File

@@ -0,0 +1,117 @@
import 'package:sqflite/sqflite.dart';
import '../../models/book_annotation.dart';
import '../database_helper.dart';
/// 书籍批注数据访问对象
class BookAnnotationDao {
final DatabaseHelper _dbHelper = DatabaseHelper.instance;
Future<Database> get _db async => await _dbHelper.database;
/// 插入批注,返回插入的 id
Future<int> insert(BookAnnotation annotation) async {
final db = await _db;
return await db.insert('book_annotations', annotation.toMap()..remove('id'));
}
/// 更新批注
Future<void> update(BookAnnotation annotation) async {
final db = await _db;
await db.update(
'book_annotations',
annotation.toMap(),
where: 'id = ?',
whereArgs: [annotation.id],
);
}
/// 保存(有 id 则更新,无 id 则插入)
Future<BookAnnotation> save(BookAnnotation annotation) async {
if (annotation.id != null) {
await update(annotation);
return annotation;
}
final id = await insert(annotation);
return annotation.copyWith(id: id);
}
/// 根据 id 删除
Future<void> deleteById(int id) async {
final db = await _db;
await db.delete('book_annotations', where: 'id = ?', whereArgs: [id]);
}
/// 根据 CFI 删除(用于删除高亮/下划线)
Future<void> deleteByCfi(String bookId, String cfi) async {
final db = await _db;
await db.delete(
'book_annotations',
where: 'book_id = ? AND cfi = ?',
whereArgs: [bookId, cfi],
);
}
/// 查询某本书的所有批注
Future<List<BookAnnotation>> getByBookId(String bookId) async {
final db = await _db;
final maps = await db.query(
'book_annotations',
where: 'book_id = ?',
whereArgs: [bookId],
orderBy: 'created_at DESC',
);
return maps.map((m) => BookAnnotation.fromMap(m)).toList();
}
/// 查询某本书的某种类型批注
Future<List<BookAnnotation>> getByBookIdAndType(String bookId, String type) async {
final db = await _db;
final maps = await db.query(
'book_annotations',
where: 'book_id = ? AND type = ?',
whereArgs: [bookId, type],
orderBy: 'created_at DESC',
);
return maps.map((m) => BookAnnotation.fromMap(m)).toList();
}
/// 查询某本书的所有书签
Future<List<BookAnnotation>> getBookmarks(String bookId) async {
return getByBookIdAndType(bookId, 'bookmark');
}
/// 查询某本书的所有高亮/下划线
Future<List<BookAnnotation>> getAnnotations(String bookId) async {
final db = await _db;
final maps = await db.query(
'book_annotations',
where: "book_id = ? AND type IN ('highlight', 'underline')",
whereArgs: [bookId],
orderBy: 'created_at DESC',
);
return maps.map((m) => BookAnnotation.fromMap(m)).toList();
}
/// 根据 id 查询
Future<BookAnnotation?> getById(int id) async {
final db = await _db;
final maps = await db.query(
'book_annotations',
where: 'id = ?',
whereArgs: [id],
limit: 1,
);
if (maps.isEmpty) return null;
return BookAnnotation.fromMap(maps.first);
}
/// 获取某本书的批注数量
Future<int> getCount(String bookId) async {
final db = await _db;
final result = await db.rawQuery(
'SELECT COUNT(*) as cnt FROM book_annotations WHERE book_id = ?',
[bookId],
);
return Sqflite.firstIntValue(result) ?? 0;
}
}

View File

@@ -0,0 +1,12 @@
/// 将归一化坐标 (0-1) 映射到 3x3 九宫格区域 (0-8)
///
/// ```
/// 0 1 2
/// 3 4 5
/// 6 7 8
/// ```
int coordinatesToPart(double x, double y) {
final col = x < 0.33 ? 0 : (x < 0.66 ? 1 : 2);
final row = y < 0.33 ? 0 : (y < 0.66 ? 1 : 2);
return row * 3 + col;
}

View File

@@ -1,6 +1,5 @@
import 'dart:convert';
import '../../service/book_server.dart';
import '../color_converter.dart';
/// 生成 foliate-js 阅读器 URL
String generateReaderUrl({
@@ -12,11 +11,11 @@ String generateReaderUrl({
}) {
final indexHtmlPath = 'http://127.0.0.1:${Server().port}/foliate-js/index.html';
final jsBg = convertDartColorToJs(backgroundColor);
final jsTc = convertDartColorToJs(textColor);
final jsBg = _convertDartColorToJs(backgroundColor);
final jsTc = _convertDartColorToJs(textColor);
final style = {
'fontSize': 100, // 100 = base 100%
'fontSize': 100,
'fontName': '',
'fontPath': '',
'fontWeight': 400,
@@ -28,7 +27,7 @@ String generateReaderUrl({
'backgroundColor': '#$jsBg',
'topMargin': 25,
'bottomMargin': 25,
'sideMargin': 15,
'sideMargin': 3,
'justify': true,
'hyphenate': false,
'pageTurnStyle': 'slide',
@@ -48,17 +47,11 @@ String generateReaderUrl({
'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
@@ -67,3 +60,14 @@ String generateReaderUrl({
return '$indexHtmlPath?$queryParts';
}
/// 将 Dart 的 ARGB hex (FFRRGGBB) 转成 CSS 的 #RRGGBB 格式
String _convertDartColorToJs(String dartColor) {
if (dartColor.startsWith('#')) {
dartColor = dartColor.substring(1);
}
if (dartColor.length == 8) {
return '#${dartColor.substring(2)}';
}
return '#$dartColor';
}