基础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

@@ -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';
}