generated from dellevin/template
基础epub阅读功能
This commit is contained in:
@@ -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) {
|
||||
|
||||
117
lib/utils/reader/book_annotation_dao.dart
Normal file
117
lib/utils/reader/book_annotation_dao.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
12
lib/utils/reader/coordinates_to_part.dart
Normal file
12
lib/utils/reader/coordinates_to_part.dart
Normal 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;
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user