优化windows笔记功能

This commit is contained in:
DelLevin-Home
2026-07-15 02:57:41 +08:00
parent 6f55ab87c7
commit 5dc8d64857
21 changed files with 983 additions and 406 deletions

View File

@@ -94,9 +94,7 @@ class _EpubDetailPageState extends State<EpubDetailPage> {
void _navigateToReader() {
if (Platform.isWindows) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Windows 桌面客户端暂不支持 EPUB 阅读功能')),
);
ToastUtil.show(context, 'Windows 桌面客户端暂不支持 EPUB 阅读功能');
return;
}
final coverPath = _linkedBookCoverPath ?? _book['cover_path'] as String?;
@@ -150,9 +148,11 @@ class _EpubDetailPageState extends State<EpubDetailPage> {
final publisher = _book['publisher'] as String? ?? '';
final isbn = _book['isbn'] as String? ?? '';
final isWin = Platform.isWindows;
return Scaffold(
backgroundColor: colors.surface,
appBar: AppBar(
appBar: isWin ? null : AppBar(
backgroundColor: colors.surface,
elevation: 0,
leading: IconButton(
@@ -167,151 +167,159 @@ class _EpubDetailPageState extends State<EpubDetailPage> {
const SizedBox(width: 4),
],
),
body: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// ── 封面 + 基本信息(横向布局)──
Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 封面
GestureDetector(
onTap: _navigateToReader,
child: SizedBox(
width: 110, height: 154,
child: _buildCover(coverPath, colors),
body: Column(children: [
// Windows: 自定义顶栏
if (isWin)
Container(
height: 52,
decoration: BoxDecoration(color: colors.surface,
border: Border(bottom: BorderSide(color: colors.outlineVariant, width: 0.5))),
child: Row(children: [
const SizedBox(width: 8),
IconButton(icon: Icon(Icons.arrow_back, color: colors.onSurface, size: 18),
onPressed: () => Navigator.pop(context)),
Expanded(child: Text(title.isNotEmpty ? title : 'EPUB 详情',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: colors.onSurface.withValues(alpha: 0.6)),
maxLines: 1, overflow: TextOverflow.ellipsis)),
IconButton(
icon: Icon(Icons.edit_outlined, size: 18, color: colors.onSurface.withValues(alpha: 0.5)),
onPressed: _navigateToEdit,
),
const SizedBox(width: 8),
]),
),
// 主体
Expanded(child: SingleChildScrollView(
child: Center(child: ConstrainedBox(constraints: const BoxConstraints(maxWidth: 720),
child: Padding(padding: EdgeInsets.symmetric(horizontal: isWin ? 48 : 16, vertical: 8),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
// ── 封面 + 基本信息(横向布局)──
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: _navigateToReader,
child: SizedBox(width: 110, height: 154, child: _buildCover(coverPath, colors)),
),
),
const SizedBox(width: 16),
// 信息
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, maxLines: 3, overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600,
color: colors.onSurface, height: 1.3)),
if (author.isNotEmpty) ...[
const SizedBox(height: 4),
Text(author, maxLines: 1, overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.5))),
],
// 元数据标签
if (_bookInfo != null) ...[
const SizedBox(height: 10),
Wrap(spacing: 6, runSpacing: 6, children: [
_buildTag('${_bookInfo!.spine.length}', colors),
_buildTag('EPUB ${_bookInfo!.epubVersion}', colors),
]),
],
const SizedBox(height: 12),
// 进度
Row(children: [
Expanded(
child: ClipRRect(
borderRadius: BorderRadius.circular(2),
child: LinearProgressIndicator(
value: progress > 0 ? progress : 0,
minHeight: 3,
backgroundColor: colors.surfaceContainerHighest,
),
),
const SizedBox(width: 16),
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(title, maxLines: 3, overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: isWin ? 20 : 18, fontWeight: FontWeight.w700, color: colors.onSurface, height: 1.3)),
if (author.isNotEmpty) ...[
const SizedBox(height: 4),
Text(author, maxLines: 1, overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.5))),
],
if (_bookInfo != null) ...[
const SizedBox(height: 10),
Wrap(spacing: 6, runSpacing: 4, children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(color: colors.primaryContainer, borderRadius: BorderRadius.circular(8)),
child: Text('${_bookInfo!.spine.length}', style: TextStyle(fontSize: 11, color: colors.onPrimaryContainer, fontWeight: FontWeight.w500)),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(color: colors.primaryContainer, borderRadius: BorderRadius.circular(8)),
child: Text('EPUB ${_bookInfo!.epubVersion}', style: TextStyle(fontSize: 11, color: colors.onPrimaryContainer, fontWeight: FontWeight.w500)),
),
const SizedBox(width: 8),
Text(progress > 0 ? '${(progress * 100).toInt()}%' : '未开始',
style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))),
]),
],
),
),
],
),
),
Divider(height: 0.5, thickness: 0.5, color: colors.outline),
// ── 描述 ──
if (description.isNotEmpty) ...[
_buildSectionHeader('简介', colors),
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
child: GestureDetector(
onTap: () => setState(() => _descriptionExpanded = !_descriptionExpanded),
child: Text(_stripHtmlTags(description),
maxLines: _descriptionExpanded ? null : 4,
overflow: _descriptionExpanded ? null : TextOverflow.ellipsis,
style: TextStyle(fontSize: 14, height: 1.7, color: colors.onSurface)),
const SizedBox(height: 12),
Row(children: [
Expanded(child: ClipRRect(borderRadius: BorderRadius.circular(2),
child: LinearProgressIndicator(value: progress > 0 ? progress : 0, minHeight: 3,
backgroundColor: colors.surfaceContainerHighest))),
const SizedBox(width: 8),
Text(progress > 0 ? '${(progress * 100).toInt()}%' : '未开始',
style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))),
]),
])),
],
),
),
Divider(height: 0.5, thickness: 0.5, color: colors.outline),
],
// ── 出版信息 ──
if (publisher.isNotEmpty || isbn.isNotEmpty) ...[
_buildSectionHeader('出版信息', colors),
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
child: Wrap(
spacing: 16,
runSpacing: 6,
children: [
if (publisher.isNotEmpty)
Row(mainAxisSize: MainAxisSize.min, children: [
const SizedBox(height: 20),
// Windows: 开始/继续阅读按钮
if (isWin)
SizedBox(width: double.infinity, height: 44,
child: FilledButton(
onPressed: _navigateToReader,
style: FilledButton.styleFrom(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
child: Text(progress > 0 ? '继续阅读' : '开始阅读',
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600)),
)),
if (isWin) const SizedBox(height: 16),
Divider(height: 0.5, thickness: 0.5, color: colors.outline),
// ── 描述 ──
if (description.isNotEmpty) ...[
_buildSectionHeader('\u7B80\u4ECB', colors),
Padding(padding: const EdgeInsets.fromLTRB(0, 0, 0, 12),
child: GestureDetector(
onTap: () => setState(() => _descriptionExpanded = !_descriptionExpanded),
child: Text(_stripHtmlTags(description),
maxLines: _descriptionExpanded ? null : 4,
overflow: _descriptionExpanded ? null : TextOverflow.ellipsis,
style: TextStyle(fontSize: 14, height: 1.7, color: colors.onSurface)),
)),
Divider(height: 0.5, thickness: 0.5, color: colors.outline),
],
// ── 出版信息 ──
if (publisher.isNotEmpty || isbn.isNotEmpty) ...[
_buildSectionHeader('\u51FA\u7248\u4FE1\u606F', colors),
Padding(padding: const EdgeInsets.fromLTRB(0, 0, 0, 12),
child: Wrap(spacing: 16, runSpacing: 6, children: [
if (publisher.isNotEmpty) Row(mainAxisSize: MainAxisSize.min, children: [
Icon(Icons.business_outlined, size: 14, color: colors.onSurface.withValues(alpha: 0.4)),
const SizedBox(width: 4),
Text(publisher, style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.7))),
]),
if (isbn.isNotEmpty)
Row(mainAxisSize: MainAxisSize.min, children: [
if (isbn.isNotEmpty) Row(mainAxisSize: MainAxisSize.min, children: [
Icon(Icons.qr_code_outlined, size: 14, color: colors.onSurface.withValues(alpha: 0.4)),
const SizedBox(width: 4),
Text(isbn, style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.7))),
]),
],
),
),
Divider(height: 0.5, thickness: 0.5, color: colors.outline),
],
])),
Divider(height: 0.5, thickness: 0.5, color: colors.outline),
],
// ── 关联书籍 ──
_buildSectionHeader('关联书籍', colors),
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 24),
child: _buildLinkedBookCard(colors),
// ── 关联书籍 ──
_buildSectionHeader('\u5173\u8054\u4E66\u7C4D', colors),
Padding(padding: const EdgeInsets.fromLTRB(0, 0, 0, 24),
child: _buildLinkedBookCard(colors)),
// ── 句读(高亮)──
Divider(height: 0.5, thickness: 0.5, color: colors.outline),
_buildHighlightsSectionHeader(colors),
Padding(padding: const EdgeInsets.fromLTRB(0, 0, 0, 24),
child: _buildHighlightsList(colors)),
// ── 书籍摘抄 ──
if ((_book['book_id'] as String? ?? '').isNotEmpty) ...[
Divider(height: 0.5, thickness: 0.5, color: colors.outline),
_buildExcerptsSectionHeader(colors),
Padding(padding: const EdgeInsets.fromLTRB(0, 0, 0, 24),
child: _buildExcerptsList(colors)),
],
// ── 其他作品 ──
if (author.isNotEmpty) ...[
Divider(height: 0.5, thickness: 0.5, color: colors.outline),
_buildSectionHeader('\u5176\u4ED6\u4F5C\u54C1', colors),
_buildOtherWorks(author, colors),
const SizedBox(height: 24),
],
]),
),
// ── 句读(高亮)──
Divider(height: 0.5, thickness: 0.5, color: colors.outline),
_buildHighlightsSectionHeader(colors),
Padding(
padding: const EdgeInsets.fromLTRB(0, 0, 0, 24),
child: _buildHighlightsList(colors),
),
// ── 书籍摘抄(仅关联书籍时显示)──
if ((_book['book_id'] as String? ?? '').isNotEmpty) ...[
Divider(height: 0.5, thickness: 0.5, color: colors.outline),
_buildExcerptsSectionHeader(colors),
Padding(
padding: const EdgeInsets.fromLTRB(0, 0, 0, 24),
child: _buildExcerptsList(colors),
),
],
// ── 其他作品(同作者)──
if (author.isNotEmpty) ...[
Divider(height: 0.5, thickness: 0.5, color: colors.outline),
_buildSectionHeader('其他作品', colors),
_buildOtherWorks(author, colors),
const SizedBox(height: 24),
],
],
),
),
bottomNavigationBar: Container(
)),
)),
]),
// 非 Windows: 底部阅读按钮
bottomNavigationBar: isWin ? null : Container(
padding: EdgeInsets.fromLTRB(16, 12, 16, 12 + MediaQuery.of(context).padding.bottom),
decoration: BoxDecoration(
color: colors.surface,
@@ -328,7 +336,7 @@ class _EpubDetailPageState extends State<EpubDetailPage> {
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
child: Text(
progress > 0 ? '继续阅读' : '开始阅读',
progress > 0 ? '\u7EE7\u7EED\u9605\u8BFB' : '\u5F00\u59CB\u9605\u8BFB',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: colors.onPrimary),
),
),
@@ -814,9 +822,7 @@ class _EpubDetailPageState extends State<EpubDetailPage> {
void _navigateToHighlight(Map<String, dynamic> highlight) {
if (Platform.isWindows) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Windows 桌面客户端暂不支持 EPUB 阅读功能')),
);
ToastUtil.show(context, 'Windows 桌面客户端暂不支持 EPUB 阅读功能');
return;
}
final chapter = int.tryParse(highlight['chapter'] as String? ?? '') ?? 0;

View File

@@ -5,6 +5,7 @@ import '../../data/epub/reader_dao.dart';
import '../../services/epub/epub_service.dart';
import '../../utils/user_prefs.dart';
import '../../utils/responsive.dart';
import '../../utils/toast_util.dart';
import 'epub_detail_page.dart';
import 'widgets/book_grid_item.dart';
@@ -84,9 +85,7 @@ class _EpubLibraryPageState extends State<EpubLibraryPage> {
if (path == null) return;
if (!path.toLowerCase().endsWith('.epub')) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('仅支持导入 .epub 格式的文件')),
);
ToastUtil.show(context, '\u4EC5\u652F\u6301\u5BFC\u5165 .epub \u683C\u5F0F\u7684\u6587\u4EF6');
}
return;
}
@@ -105,9 +104,7 @@ class _EpubLibraryPageState extends State<EpubLibraryPage> {
if (imported != null) {
await _loadBooks();
} else if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('EPUB 解析失败,请检查文件')),
);
ToastUtil.show(context, 'EPUB \u89E3\u6790\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u6587\u4EF6');
}
}
@@ -227,9 +224,10 @@ class _EpubLibraryPageState extends State<EpubLibraryPage> {
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
final isWin = Platform.isWindows;
return Scaffold(
backgroundColor: colors.surface,
appBar: AppBar(
appBar: isWin ? null : AppBar(
backgroundColor: colors.surface,
elevation: 0,
title: _isSearching
@@ -250,43 +248,72 @@ class _EpubLibraryPageState extends State<EpubLibraryPage> {
icon: Icon(_isSearching ? Icons.close : Icons.arrow_back, size: 20),
onPressed: _isSearching ? _toggleSearch : () => Navigator.pop(context),
),
actions: [
if (!_isSearching)
IconButton(
icon: Icon(Icons.search, size: 20, color: colors.onSurface.withValues(alpha: 0.6)),
onPressed: _toggleSearch,
),
IconButton(
icon: Icon(
_viewMode == ViewMode.relaxed
? Icons.view_compact_outlined
: Icons.view_agenda_outlined,
size: 20,
color: colors.onSurface.withValues(alpha: 0.6),
),
onPressed: _toggleViewMode,
),
IconButton(
icon: Icon(Icons.sort, size: 20, color: colors.onSurface.withValues(alpha: 0.6)),
onPressed: _showSortMenu,
),
IconButton(
icon: Icon(Icons.add_outlined, size: 20, color: colors.onSurface.withValues(alpha: 0.6)),
onPressed: _pickAndImport,
),
const SizedBox(width: 4),
],
actions: _buildActions(colors),
),
body: _isLoading
? Center(child: CircularProgressIndicator(color: colors.primary))
: _books.isEmpty
? _buildEmpty(colors)
: _filteredBooks.isEmpty
? Center(child: Text('无搜索结果', style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.35))))
: _buildGrid(colors),
body: Column(children: [
// Windows: 自定义顶栏
if (isWin)
Container(
height: 52,
decoration: BoxDecoration(color: colors.surface,
border: Border(bottom: BorderSide(color: colors.outlineVariant, width: 0.5))),
child: Row(children: [
const SizedBox(width: 8),
IconButton(icon: Icon(_isSearching ? Icons.close : Icons.arrow_back, color: colors.onSurface, size: 18),
onPressed: _isSearching ? _toggleSearch : () => Navigator.pop(context)),
Expanded(child: _isSearching
? TextField(controller: _searchCtrl, autofocus: true,
style: TextStyle(fontSize: 14, color: colors.onSurface),
decoration: InputDecoration(hintText: '搜索书名或作者',
hintStyle: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.35)),
border: InputBorder.none, isDense: true, contentPadding: EdgeInsets.zero),
onChanged: (_) => _onSearchChanged())
: Text('EPUB 阅读',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: colors.onSurface.withValues(alpha: 0.6)))),
..._buildActions(colors),
]),
),
// 主体
Expanded(child: _isLoading
? Center(child: CircularProgressIndicator(color: colors.primary))
: _books.isEmpty
? _buildEmpty(colors)
: _filteredBooks.isEmpty
? Center(child: Text('无搜索结果', style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.35))))
: _buildGrid(colors)),
]),
);
}
List<Widget> _buildActions(ColorScheme colors) {
return [
if (!_isSearching)
IconButton(
icon: Icon(Icons.search, size: 20, color: colors.onSurface.withValues(alpha: 0.6)),
onPressed: _toggleSearch,
),
IconButton(
icon: Icon(
_viewMode == ViewMode.relaxed
? Icons.view_compact_outlined
: Icons.view_agenda_outlined,
size: 20,
color: colors.onSurface.withValues(alpha: 0.6),
),
onPressed: _toggleViewMode,
),
IconButton(
icon: Icon(Icons.sort, size: 20, color: colors.onSurface.withValues(alpha: 0.6)),
onPressed: _showSortMenu,
),
IconButton(
icon: Icon(Icons.add_outlined, size: 20, color: colors.onSurface.withValues(alpha: 0.6)),
onPressed: _pickAndImport,
),
const SizedBox(width: 4),
];
}
Widget _buildEmpty(ColorScheme colors) {
return Center(
child: Padding(