diff --git a/assets/vditor/dist/vditor_editor.html b/assets/vditor/dist/vditor_editor.html
index 96ec7c0..c7d1a88 100644
--- a/assets/vditor/dist/vditor_editor.html
+++ b/assets/vditor/dist/vditor_editor.html
@@ -13,6 +13,12 @@
.vditor-ir__marker--cursor,
.vditor-content,
.vditor-reset { background: transparent !important; }
+ /* 所有滚动条统一:极细极淡 */
+ *::-webkit-scrollbar { width: 4px; height: 4px; }
+ *::-webkit-scrollbar-track { background: transparent; }
+ *::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.06); border-radius: 2px; }
+ *::-webkit-scrollbar-thumb:hover { background: rgba(0,0,0,0.15); }
+ *::-webkit-scrollbar-corner { background: transparent; }
@@ -72,6 +78,12 @@
}
}
+ function setBgColor(color) {
+ document.body.style.backgroundColor = color;
+ const el = document.querySelector('.vditor-ir') || document.querySelector('.vditor-wysiwyg');
+ if (el) el.style.backgroundColor = color;
+ }
+
function insertValue(text) {
if (vditor) vditor.insertValue(text);
}
diff --git a/lib/data/note/note_dao.dart b/lib/data/note/note_dao.dart
index 8e598df..735f347 100644
--- a/lib/data/note/note_dao.dart
+++ b/lib/data/note/note_dao.dart
@@ -65,9 +65,12 @@ class NoteDao {
// 更新笔记
Future updateNote(Note note) => _wrap('updateNote', () async {
final db = await _dbHelper.database;
+ final data = note.toJson();
+ // 不覆盖 created_at,保持创建时间不变
+ data.remove('created_at');
return await db.update(
'notes',
- note.toJson(),
+ data,
where: 'id = ?',
whereArgs: [note.id],
);
diff --git a/lib/main.dart b/lib/main.dart
index 79f5248..fb710bb 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -10,6 +10,7 @@ import 'package:url_launcher/url_launcher.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
+import 'package:window_manager/window_manager.dart';
import 'pages/home/home_page.dart';
import 'utils/theme/app_theme.dart';
import 'utils/app_router.dart';
@@ -17,6 +18,7 @@ import 'utils/user_prefs.dart';
import 'services/changelog_service.dart';
import 'services/usage_stats_service.dart';
import 'providers/app_provider.dart';
+import 'widgets/app_shell.dart';
final RouteObserver> routeObserver = RouteObserver>();
@@ -29,6 +31,13 @@ void main() async {
if (Platform.isWindows) {
sqfliteFfiInit();
databaseFactory = databaseFactoryFfi;
+ // 初始化 window_manager:隐藏原生标题栏
+ await windowManager.ensureInitialized();
+ windowManager.waitUntilReadyToShow().then((_) async {
+ await windowManager.setTitleBarStyle(TitleBarStyle.hidden);
+ await windowManager.setMinimumSize(const Size(900, 640));
+ await windowManager.show();
+ });
// 注册 epub:// 自定义协议,使 WebView2 能拦截该协议的请求
try {
windowsWebViewEnvironment = await WebViewEnvironment.create(settings:
@@ -243,6 +252,10 @@ class _MyAppState extends State with WidgetsBindingObserver {
systemNavigationBarColor: isDark ? Colors.black : Colors.white,
systemNavigationBarIconBrightness: isDark ? Brightness.light : Brightness.dark,
));
+ // Windows: 同步窗口边框明暗
+ if (Platform.isWindows) {
+ windowManager.setBrightness(isDark ? Brightness.dark : Brightness.light);
+ }
}
@override
@@ -305,6 +318,7 @@ class _MyAppState extends State with WidgetsBindingObserver {
theme: light,
darkTheme: dark,
themeMode: provider.themeMode,
+ builder: (ctx, nav) => AppShell(child: nav!),
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
diff --git a/lib/pages/book/book_detail_page.dart b/lib/pages/book/book_detail_page.dart
index 848ab0a..6f3f503 100644
--- a/lib/pages/book/book_detail_page.dart
+++ b/lib/pages/book/book_detail_page.dart
@@ -582,9 +582,8 @@ class _BookDetailPageState extends State {
child: Row(children: [
Icon(Icons.calendar_today_outlined, size: 14, color: colors.onSurface.withValues(alpha: 0.4)),
const SizedBox(width: 8),
- Text(hasDate ? '${date!.year}.${date!.month.toString().padLeft(2, '0')}.${date!.day.toString().padLeft(2, '0')}' : '选择日期',
- style: TextStyle(fontSize: 14, color: hasDate ? colors.onSurface : colors.onSurface.withValues(alpha: 0.25))),
- const Spacer(),
+ Expanded(child: Text(hasDate ? '${date!.year}.${date!.month.toString().padLeft(2, '0')}.${date!.day.toString().padLeft(2, '0')}' : '\u9009\u62E9\u65E5\u671F',
+ style: TextStyle(fontSize: 14, color: hasDate ? colors.onSurface : colors.onSurface.withValues(alpha: 0.25)), overflow: TextOverflow.ellipsis)),
if (clearable && hasDate) GestureDetector(onTap: () => onChanged(null),
child: Icon(Icons.close, size: 14, color: colors.onSurface.withValues(alpha: 0.3))),
]))),
@@ -743,9 +742,7 @@ class _BookDetailPageState extends State {
FilledButton.tonalIcon(
onPressed: () {
if (Platform.isWindows) {
- ScaffoldMessenger.of(context).showSnackBar(
- const SnackBar(content: Text('Windows 桌面客户端暂不支持 EPUB 阅读功能')),
- );
+ ToastUtil.show(context, 'Windows 桌面客户端暂不支持 EPUB 阅读功能');
return;
}
Navigator.push(context, MaterialPageRoute(
diff --git a/lib/pages/epub_reader/epub_detail_page.dart b/lib/pages/epub_reader/epub_detail_page.dart
index c43b3e9..ad460a3 100644
--- a/lib/pages/epub_reader/epub_detail_page.dart
+++ b/lib/pages/epub_reader/epub_detail_page.dart
@@ -94,9 +94,7 @@ class _EpubDetailPageState extends State {
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 {
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 {
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 {
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 {
void _navigateToHighlight(Map 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;
diff --git a/lib/pages/epub_reader/epub_library_page.dart b/lib/pages/epub_reader/epub_library_page.dart
index 15d7b6f..97df5c2 100644
--- a/lib/pages/epub_reader/epub_library_page.dart
+++ b/lib/pages/epub_reader/epub_library_page.dart
@@ -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 {
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 {
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 {
@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 {
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 _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(
diff --git a/lib/pages/home/home_page.dart b/lib/pages/home/home_page.dart
index ab71a6c..5d73bf2 100644
--- a/lib/pages/home/home_page.dart
+++ b/lib/pages/home/home_page.dart
@@ -324,7 +324,7 @@ class _DesktopIconRail extends StatelessWidget {
width: 160,
child: Column(
children: [
- SizedBox(height: MediaQuery.of(context).padding.top + 8),
+ SizedBox(height: (Platform.isWindows ? 0 : MediaQuery.of(context).padding.top) + 8),
// 头像 + 昵称 + 座右铭
_buildProfileHeader(context),
const SizedBox(height: 10),
@@ -3531,7 +3531,7 @@ class _SearchDialogState extends State<_SearchDialog> {
Widget _toggleBtn(String label, bool selected, VoidCallback onTap, ColorScheme colors) {
return Material(
- color: selected ? colors.onSurface : Colors.transparent,
+ color: selected ? colors.primary : Colors.transparent,
borderRadius: BorderRadius.circular(8),
child: InkWell(
onTap: onTap,
@@ -3544,7 +3544,7 @@ class _SearchDialogState extends State<_SearchDialog> {
Icon(
selected ? Icons.search_rounded : Icons.search_outlined,
size: 14,
- color: selected ? colors.surface : colors.onSurface.withValues(alpha: 0.5),
+ color: selected ? colors.onPrimary : colors.onSurface.withValues(alpha: 0.5),
),
const SizedBox(width: 5),
Text(
@@ -3552,7 +3552,7 @@ class _SearchDialogState extends State<_SearchDialog> {
style: TextStyle(
fontSize: 12,
fontWeight: selected ? FontWeight.w600 : FontWeight.w400,
- color: selected ? colors.surface : colors.onSurface.withValues(alpha: 0.55),
+ color: selected ? colors.onPrimary : colors.onSurface.withValues(alpha: 0.55),
),
),
],
@@ -3664,7 +3664,7 @@ class _DesktopListPanelState extends State<_DesktopListPanel> {
return Column(
children: [
// 顶部搜索栏
- SizedBox(height: MediaQuery.of(context).padding.top),
+ SizedBox(height: Platform.isWindows ? 0 : MediaQuery.of(context).padding.top),
Padding(
padding: const EdgeInsets.fromLTRB(12, 8, 12, 4),
child: Row(
@@ -3939,21 +3939,20 @@ class _DesktopListPanelState extends State<_DesktopListPanel> {
Widget _buildNoteList(BuildContext context) {
return Consumer(
builder: (context, provider, _) {
- final items = provider.notes.where((n) => !n.isDeleted).toList();
+ var items = provider.notes.where((n) => !n.isDeleted).toList();
+ // 置顶排前面,同组内按创建时间倒序
+ items.sort((a, b) {
+ if (a.isPinned != b.isPinned) return a.isPinned ? -1 : 1;
+ return b.createdAt.compareTo(a.createdAt);
+ });
if (items.isEmpty) return _buildEmpty('暂无笔记记录', Icons.note_outlined);
return ListView.builder(
padding: const EdgeInsets.symmetric(vertical: 4),
itemCount: items.length,
- itemBuilder: (_, i) => _CompactListItem(
- title: items[i].title.isNotEmpty ? items[i].title : '随手记',
- subtitle: items[i].content.length > 40 ? '${items[i].content.substring(0, 40)}...' : (items[i].content.isNotEmpty ? items[i].content : null),
- imagePath: null,
- accentColor: const Color(0xFF9333EA),
- icon: Icons.note_outlined,
+ itemBuilder: (_, i) => _DesktopNoteItem(
+ note: items[i],
selected: provider.selectedNote?.id == items[i].id,
- onTap: () {
- provider.selectNote(items[i]);
- },
+ onTap: () => provider.selectNote(items[i]),
),
);
},
@@ -4273,6 +4272,154 @@ class _CompactListItem extends StatelessWidget {
}
}
+// ─── 桌面端笔记列表项 ──────────────────────────────────────
+
+class _DesktopNoteItem extends StatelessWidget {
+ final Note note;
+ final bool selected;
+ final VoidCallback onTap;
+
+ const _DesktopNoteItem({
+ required this.note,
+ required this.selected,
+ required this.onTap,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ final colors = Theme.of(context).colorScheme;
+ final isDark = colors.brightness == Brightness.dark;
+ final title = note.title.isNotEmpty ? note.title : '随手记';
+ final preview = note.content.length > 60 ? '${note.content.substring(0, 60)}...' : note.content;
+ final dateStr = '${note.updatedAt.month}/${note.updatedAt.day}';
+
+ return Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
+ child: Material(
+ color: selected
+ ? colors.primary.withValues(alpha: isDark ? 0.12 : 0.06)
+ : Colors.transparent,
+ borderRadius: BorderRadius.circular(8),
+ child: InkWell(
+ onTap: onTap,
+ borderRadius: BorderRadius.circular(8),
+ onSecondaryTapUp: (details) => _showContextMenu(context, details),
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(children: [
+ if (note.isPinned) ...[
+ Icon(Icons.push_pin, size: 12, color: colors.primary),
+ const SizedBox(width: 4),
+ ],
+ Expanded(child: Text(title, maxLines: 1, overflow: TextOverflow.ellipsis,
+ style: TextStyle(fontSize: 13, fontWeight: selected ? FontWeight.w600 : FontWeight.w500,
+ color: selected ? colors.primary : colors.onSurface))),
+ const SizedBox(width: 6),
+ Text(dateStr, style: TextStyle(fontSize: 10, color: colors.onSurface.withValues(alpha: 0.3))),
+ ]),
+ if (preview.isNotEmpty) ...[
+ const SizedBox(height: 3),
+ Text(preview, maxLines: 1, overflow: TextOverflow.ellipsis,
+ style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.4), height: 1.4)),
+ ],
+ if (note.tags.isNotEmpty) ...[
+ const SizedBox(height: 5),
+ Wrap(spacing: 4, runSpacing: 2, children: [
+ for (final tag in note.tags.take(3))
+ Container(
+ padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1),
+ decoration: BoxDecoration(
+ color: colors.primaryContainer.withValues(alpha: 0.5),
+ borderRadius: BorderRadius.circular(4),
+ ),
+ child: Text(tag, style: TextStyle(fontSize: 9, color: colors.onPrimaryContainer)),
+ ),
+ if (note.tags.length > 3)
+ Text('+${note.tags.length - 3}', style: TextStyle(fontSize: 9, color: colors.onSurface.withValues(alpha: 0.3))),
+ ]),
+ ],
+ ],
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+
+ void _showContextMenu(BuildContext context, TapUpDetails details) {
+ final provider = context.read();
+ final overlay = Overlay.of(context);
+ final renderBox = context.findRenderObject() as RenderBox;
+ final position = renderBox.localToGlobal(details.localPosition);
+
+ showMenu(
+ context: context,
+ position: RelativeRect.fromLTRB(position.dx, position.dy, position.dx + 1, position.dy + 1),
+ items: [
+ PopupMenuItem(
+ value: 'pin',
+ height: 36,
+ child: Row(children: [
+ Icon(note.isPinned ? Icons.push_pin_outlined : Icons.push_pin, size: 16, color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6)),
+ const SizedBox(width: 8),
+ Text(note.isPinned ? '取消置顶' : '置顶', style: const TextStyle(fontSize: 13)),
+ ]),
+ ),
+ PopupMenuItem(
+ value: 'edit',
+ height: 36,
+ child: Row(children: [
+ Icon(Icons.edit_outlined, size: 16, color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6)),
+ const SizedBox(width: 8),
+ const Text('编辑', style: TextStyle(fontSize: 13)),
+ ]),
+ ),
+ PopupMenuItem(
+ value: 'delete',
+ height: 36,
+ child: Row(children: [
+ Icon(Icons.delete_outline, size: 16, color: Theme.of(context).colorScheme.error),
+ const SizedBox(width: 8),
+ Text('删除', style: TextStyle(color: Theme.of(context).colorScheme.error, fontSize: 13)),
+ ]),
+ ),
+ ],
+ ).then((value) async {
+ if (value == null || !context.mounted) return;
+ switch (value) {
+ case 'pin':
+ await provider.toggleNotePin(note.id, !note.isPinned);
+ break;
+ case 'edit':
+ provider.selectNote(note);
+ // 进入编辑模式由 NoteDetailPage 处理
+ break;
+ case 'delete':
+ final confirmed = await showDialog(
+ context: context,
+ builder: (ctx) => AlertDialog(
+ backgroundColor: Theme.of(ctx).colorScheme.surface,
+ title: const Text('确认删除'),
+ content: Text('确定要删除「${note.title.isNotEmpty ? note.title : '随手记'}」吗?'),
+ actions: [
+ TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('取消')),
+ TextButton(onPressed: () => Navigator.pop(ctx, true),
+ child: Text('删除', style: TextStyle(color: Theme.of(ctx).colorScheme.error))),
+ ],
+ ),
+ );
+ if (confirmed == true && context.mounted) {
+ await provider.removeNote(note.id);
+ }
+ break;
+ }
+ });
+ }
+}
+
// ─── 搜索分组标题 ──────────────────────────────────────
class _SearchGroupHeader extends StatelessWidget {
diff --git a/lib/pages/note/note_add_page.dart b/lib/pages/note/note_add_page.dart
index 68d0c45..54e5d94 100644
--- a/lib/pages/note/note_add_page.dart
+++ b/lib/pages/note/note_add_page.dart
@@ -57,19 +57,20 @@ class _NoteAddPageState extends State {
children: [
// 顶栏
Container(
- height: 48,
+ 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.close, color: colors.onSurface, size: 18),
onPressed: () => widget.onCancel?.call(),
),
Expanded(
child: Text('添加笔记',
- style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: colors.onSurface)),
+ style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: colors.onSurface.withValues(alpha: 0.6))),
),
// 编辑/预览切换
Container(
@@ -80,7 +81,7 @@ class _NoteAddPageState extends State {
_editModeChip(Icons.visibility_outlined, '预览', 'preview', colors),
]),
),
- const SizedBox(width: 8),
+ const SizedBox(width: 12),
FilledButton.icon(
onPressed: _save,
icon: const Icon(Icons.check, size: 16),
@@ -90,7 +91,7 @@ class _NoteAddPageState extends State {
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
),
- const SizedBox(width: 12),
+ const SizedBox(width: 16),
]),
),
// 主体
@@ -126,82 +127,91 @@ class _NoteAddPageState extends State {
Widget _buildEditArea(ColorScheme colors) {
final isWin = Platform.isWindows;
return Column(children: [
- // 标题输入
+ // 标题输入(Windows: 更大更醒目)
Container(
- padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+ padding: EdgeInsets.symmetric(horizontal: isWin ? 48 : 16, vertical: isWin ? 16 : 8),
decoration: BoxDecoration(border: Border(bottom: BorderSide(color: colors.outlineVariant, width: 0.5))),
- child: TextField(
- controller: _titleCtrl,
- maxLines: 1,
- style: TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: colors.onSurface),
- decoration: InputDecoration(
- hintText: '添加标题',
- hintStyle: TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: colors.onSurface.withValues(alpha: 0.2)),
- border: InputBorder.none, enabledBorder: InputBorder.none, focusedBorder: InputBorder.none,
- isDense: true,
- contentPadding: EdgeInsets.zero,
+ child: Center(child: ConstrainedBox(constraints: const BoxConstraints(maxWidth: 720),
+ child: TextField(
+ controller: _titleCtrl,
+ maxLines: 1,
+ style: TextStyle(fontSize: isWin ? 22 : 16, fontWeight: FontWeight.w700, color: colors.onSurface, height: 1.4),
+ decoration: InputDecoration(
+ hintText: '添加标题',
+ hintStyle: TextStyle(fontSize: isWin ? 22 : 16, fontWeight: FontWeight.w700, color: colors.onSurface.withValues(alpha: 0.2), height: 1.4),
+ border: InputBorder.none, enabledBorder: InputBorder.none, focusedBorder: InputBorder.none,
+ isDense: true,
+ contentPadding: EdgeInsets.zero,
+ ),
+ onChanged: (_) => setState(() {}),
),
- onChanged: (_) => setState(() {}),
- ),
+ )),
),
- // Windows: 标签栏移到标题下方(靠左)
+ // Windows: 标签栏(彩色药丸样式)
if (isWin)
Container(
- padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
- decoration: BoxDecoration(border: Border(bottom: BorderSide(color: colors.outlineVariant, width: 0.5))),
- child: Align(
- alignment: Alignment.centerLeft,
- child: SingleChildScrollView(
- scrollDirection: Axis.horizontal,
- child: Row(mainAxisSize: MainAxisSize.min, children: [
- for (int i = 0; i < _tags.length; i++)
- Padding(
- padding: const EdgeInsets.only(right: 4),
- child: Container(
- padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
- decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(6)),
- child: Row(mainAxisSize: MainAxisSize.min, children: [
- Text(_tags[i], style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.6))),
- const SizedBox(width: 3),
- GestureDetector(
- onTap: () => setState(() => _tags.removeAt(i)),
- child: Icon(Icons.close, size: 10, color: colors.onSurface.withValues(alpha: 0.3)),
+ padding: const EdgeInsets.symmetric(horizontal: 48, vertical: 8),
+ child: Center(child: ConstrainedBox(constraints: const BoxConstraints(maxWidth: 720),
+ child: Align(
+ alignment: Alignment.centerLeft,
+ child: SingleChildScrollView(
+ scrollDirection: Axis.horizontal,
+ child: Row(mainAxisSize: MainAxisSize.min, children: [
+ for (int i = 0; i < _tags.length; i++)
+ Padding(
+ padding: const EdgeInsets.only(right: 6),
+ child: Container(
+ padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
+ decoration: BoxDecoration(
+ color: colors.primaryContainer,
+ borderRadius: BorderRadius.circular(12),
),
+ child: Row(mainAxisSize: MainAxisSize.min, children: [
+ Text(_tags[i], style: TextStyle(fontSize: 12, color: colors.onPrimaryContainer, fontWeight: FontWeight.w500)),
+ const SizedBox(width: 4),
+ GestureDetector(
+ onTap: () => setState(() => _tags.removeAt(i)),
+ child: Icon(Icons.close, size: 12, color: colors.onPrimaryContainer.withValues(alpha: 0.6)),
+ ),
+ ]),
+ ),
+ ),
+ GestureDetector(
+ onTap: _showTagPanel,
+ child: Container(
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5),
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(12),
+ border: Border.all(color: colors.outline.withValues(alpha: 0.3), width: 1),
+ ),
+ child: Row(mainAxisSize: MainAxisSize.min, children: [
+ Icon(Icons.add, size: 14, color: colors.onSurface.withValues(alpha: 0.4)),
+ const SizedBox(width: 3),
+ Text('标签', style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))),
]),
),
),
- GestureDetector(
- onTap: _showTagPanel,
- child: Container(
- padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
- decoration: BoxDecoration(
- borderRadius: BorderRadius.circular(6),
- border: Border.all(color: colors.onSurface.withValues(alpha: 0.25), width: 1),
- ),
- child: Row(mainAxisSize: MainAxisSize.min, children: [
- Icon(Icons.add, size: 12, color: colors.onSurface.withValues(alpha: 0.35)),
- const SizedBox(width: 2),
- Text('标签', style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.35))),
- ]),
- ),
- ),
- ]),
+ ]),
+ ),
),
- ),
+ )),
),
- // 内容编辑
+ // 内容编辑(Windows: 限宽居中)
Expanded(
child: isWin
- ? VditorEditor(
- key: _vditorKey,
- initialContent: _contentCtrl.text,
- noteId: _tempId,
- isDark: Theme.of(context).brightness == Brightness.dark,
- onContentChanged: (value) {
- _contentCtrl.text = value;
- setState(() {});
- },
- )
+ ? Center(child: ConstrainedBox(constraints: const BoxConstraints(maxWidth: 720),
+ child: VditorEditor(
+ key: _vditorKey,
+ initialContent: _contentCtrl.text,
+ noteId: _tempId,
+ isDark: Theme.of(context).brightness == Brightness.dark,
+ surfaceColor: colors.surface,
+ onContentChanged: (value) {
+ _contentCtrl.text = value;
+ setState(() {});
+ },
+ ),
+ ))
: TextField(
controller: _contentCtrl,
maxLines: null,
@@ -284,14 +294,15 @@ class _NoteAddPageState extends State {
]),
]),
),
- // Windows: 底部只显示字数
+ // Windows: 底部字数(简洁)
if (isWin)
Container(
- padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
- decoration: BoxDecoration(border: Border(top: BorderSide(color: colors.outlineVariant, width: 0.5))),
- child: Row(mainAxisAlignment: MainAxisAlignment.end, children: [
- Text('${_contentCtrl.text.length} 字', style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.3))),
- ]),
+ padding: const EdgeInsets.symmetric(horizontal: 48, vertical: 8),
+ child: Center(child: ConstrainedBox(constraints: const BoxConstraints(maxWidth: 720),
+ child: Row(mainAxisAlignment: MainAxisAlignment.end, children: [
+ Text('${_contentCtrl.text.length} 字', style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.3))),
+ ]),
+ )),
),
]);
}
@@ -526,9 +537,12 @@ class _NoteAddPageState extends State {
Future _save() async {
final title = _titleCtrl.text.trim();
- final content = Platform.isWindows
- ? (await _vditorKey.currentState?.getValue() ?? '').trim()
- : _contentCtrl.text.trim();
+ String content;
+ if (Platform.isWindows && _vditorKey.currentState != null && _vditorKey.currentState!.isReady) {
+ content = (await _vditorKey.currentState!.getValue()).trim();
+ } else {
+ content = _contentCtrl.text.trim();
+ }
if (title.isEmpty && content.isEmpty) {
ToastUtil.show(context, '标题或内容不能为空');
return;
diff --git a/lib/pages/note/note_detail_page.dart b/lib/pages/note/note_detail_page.dart
index 8a9dde8..151f17c 100644
--- a/lib/pages/note/note_detail_page.dart
+++ b/lib/pages/note/note_detail_page.dart
@@ -78,9 +78,12 @@ class _NoteDetailPageState extends State {
}
Future _autoSave() async {
- final content = Platform.isWindows
- ? (await _vditorKey.currentState?.getValue() ?? '').trim()
- : _contentCtrl.text.trim();
+ String content;
+ if (Platform.isWindows && _vditorKey.currentState != null && _vditorKey.currentState!.isReady) {
+ content = (await _vditorKey.currentState!.getValue()).trim();
+ } else {
+ content = _contentCtrl.text.trim();
+ }
final title = _titleCtrl.text.trim();
if (title.isEmpty && content.isEmpty) return;
try {
@@ -103,9 +106,12 @@ class _NoteDetailPageState extends State {
Future _saveEdit() async {
_autoSaveTimer?.cancel();
final title = _titleCtrl.text.trim();
- final content = Platform.isWindows
- ? (await _vditorKey.currentState?.getValue() ?? '').trim()
- : _contentCtrl.text.trim();
+ String content;
+ if (Platform.isWindows && _vditorKey.currentState != null && _vditorKey.currentState!.isReady) {
+ content = (await _vditorKey.currentState!.getValue()).trim();
+ } else {
+ content = _contentCtrl.text.trim();
+ }
if (title.isEmpty && content.isEmpty) {
ToastUtil.show(context, '标题或内容不能为空');
return;
@@ -241,12 +247,13 @@ class _NoteDetailPageState extends State {
children: [
// 顶栏
Container(
- height: 48,
+ 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: widget.embedded
@@ -256,55 +263,75 @@ class _NoteDetailPageState extends State {
Expanded(
child: Text(
note.title.isNotEmpty ? note.title : _truncateContent(note.content),
- style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: colors.onSurface),
+ style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: colors.onSurface.withValues(alpha: 0.6)),
maxLines: 1, overflow: TextOverflow.ellipsis),
),
- const SizedBox(width: 4),
+ const SizedBox(width: 16),
]),
),
- // 日期信息栏
- Container(
- padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 6),
- decoration: BoxDecoration(
- border: Border(bottom: BorderSide(color: colors.outlineVariant, width: 0.5)),
- ),
- child: Row(
+ // 内容区(限宽居中)
+ Expanded(
+ child: ListView(
+ padding: const EdgeInsets.symmetric(vertical: 32),
children: [
- Text('${note.createdAt.day}',
- style: TextStyle(fontSize: 30, fontWeight: FontWeight.w200, color: colors.onSurface.withValues(alpha: 0.75), height: 1.0)),
- const SizedBox(width: 8),
- Column(crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [
- Text('${note.createdAt.year}/${note.createdAt.month.toString().padLeft(2, '0')} 周${_weekdays[note.createdAt.weekday - 1]}',
- style: TextStyle(fontSize: 11, fontWeight: FontWeight.w500, color: colors.onSurface.withValues(alpha: 0.55))),
- const SizedBox(height: 1),
- Text('${note.createdAt.hour.toString().padLeft(2, '0')}:${note.createdAt.minute.toString().padLeft(2, '0')}',
- style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.4))),
- ]),
- const Spacer(),
- Text('${note.content.length} 字',
- style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.35))),
+ Center(child: ConstrainedBox(constraints: const BoxConstraints(maxWidth: 720),
+ child: Padding(padding: const EdgeInsets.symmetric(horizontal: 48),
+ child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
+ // 标题
+ if (note.title.isNotEmpty)
+ Text(note.title, style: TextStyle(fontSize: 28, fontWeight: FontWeight.w700, color: colors.onSurface, height: 1.3)),
+ // 日期 + 字数
+ const SizedBox(height: 12),
+ Row(children: [
+ Text('${note.createdAt.year}/${note.createdAt.month.toString().padLeft(2, '0')}/${note.createdAt.day.toString().padLeft(2, '0')} 周${_weekdays[note.createdAt.weekday - 1]}',
+ style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))),
+ const SizedBox(width: 12),
+ Text('${note.createdAt.hour.toString().padLeft(2, '0')}:${note.createdAt.minute.toString().padLeft(2, '0')}',
+ style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))),
+ const SizedBox(width: 12),
+ Text('${note.content.length} 字',
+ style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))),
+ ]),
+ // 标签
+ if (note.tags.isNotEmpty) ...[
+ const SizedBox(height: 12),
+ Wrap(spacing: 6, runSpacing: 4, children: [
+ for (final tag in note.tags)
+ Container(
+ padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
+ decoration: BoxDecoration(
+ color: colors.primaryContainer,
+ borderRadius: BorderRadius.circular(12),
+ ),
+ child: Text(tag, style: TextStyle(fontSize: 12, color: colors.onPrimaryContainer, fontWeight: FontWeight.w500)),
+ ),
+ ]),
+ ],
+ const SizedBox(height: 24),
+ // Markdown 内容
+ Markdown(
+ data: note.content,
+ styleSheet: _buildMarkdownStyleSheet(colors),
+ padding: EdgeInsets.zero,
+ shrinkWrap: true,
+ physics: const NeverScrollableScrollPhysics(),
+ // ignore: deprecated_member_use
+ imageBuilder: (uri, title, alt) => _buildMarkdownImage(uri, note),
+ ),
+ if (note.images.isNotEmpty) ...[
+ const SizedBox(height: 16),
+ _buildImageRow(note.images),
+ ],
+ const SizedBox(height: 48),
+ ]),
+ ),
+ )),
],
),
),
- if (note.tags.isNotEmpty)
- Padding(
- padding: const EdgeInsets.only(top: 6, left: 24, right: 24),
- child: _buildTagRow(note.tags),
- ),
- // 内容
- Expanded(
- child: Markdown(
- data: note.content,
- styleSheet: _buildMarkdownStyleSheet(colors),
- padding: const EdgeInsets.all(24),
- // ignore: deprecated_member_use
- imageBuilder: (uri, title, alt) => _buildMarkdownImage(uri, note),
- ),
- ),
- if (note.images.isNotEmpty) _buildImageRow(note.images),
// 底部操作栏
Container(
- height: 56,
+ height: 52,
decoration: BoxDecoration(
color: colors.surface,
border: Border(top: BorderSide(color: colors.outlineVariant, width: 0.5)),
@@ -348,14 +375,15 @@ class _NoteDetailPageState extends State {
children: [
// 顶栏
Container(
- height: 48,
+ 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.close, color: colors.onSurface, size: 18),
onPressed: () { _autoSaveTimer?.cancel(); if (_saveStatus == 'saved') _autoSave(); setState(() => _isEditing = false); }),
Expanded(child: Text(_titleCtrl.text.isNotEmpty ? _titleCtrl.text : '编辑笔记',
- style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: colors.onSurface),
+ style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: colors.onSurface.withValues(alpha: 0.6)),
maxLines: 1, overflow: TextOverflow.ellipsis)),
// 编辑/预览切换
Container(
@@ -366,7 +394,7 @@ class _NoteDetailPageState extends State {
_editModeChip(Icons.visibility_outlined, '预览', 'preview', colors),
]),
),
- const SizedBox(width: 8),
+ const SizedBox(width: 12),
if (_saveStatus == 'saved')
Padding(padding: const EdgeInsets.only(right: 8),
child: Row(mainAxisSize: MainAxisSize.min, children: [
@@ -378,7 +406,7 @@ class _NoteDetailPageState extends State {
icon: const Icon(Icons.check, size: 16), label: const Text('保存'),
style: FilledButton.styleFrom(padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)))),
- const SizedBox(width: 12),
+ const SizedBox(width: 16),
]),
),
// 主体
@@ -409,75 +437,84 @@ class _NoteDetailPageState extends State {
Widget _buildEditArea(ColorScheme colors, Note note) {
final isWin = Platform.isWindows;
return Column(children: [
- // 标题输入
+ // 标题输入(Windows: 更大更醒目)
Container(
- padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+ padding: EdgeInsets.symmetric(horizontal: isWin ? 48 : 16, vertical: isWin ? 16 : 8),
decoration: BoxDecoration(border: Border(bottom: BorderSide(color: colors.outlineVariant, width: 0.5))),
- child: TextField(controller: _titleCtrl, maxLines: 1,
- style: TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: colors.onSurface),
- decoration: InputDecoration(hintText: '添加标题',
- hintStyle: TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: colors.onSurface.withValues(alpha: 0.2)),
- border: InputBorder.none, enabledBorder: InputBorder.none, focusedBorder: InputBorder.none, isDense: true, contentPadding: EdgeInsets.zero),
- onChanged: (_) => setState(() {})),
+ child: Center(child: ConstrainedBox(constraints: const BoxConstraints(maxWidth: 720),
+ child: TextField(controller: _titleCtrl, maxLines: 1,
+ style: TextStyle(fontSize: isWin ? 22 : 16, fontWeight: FontWeight.w700, color: colors.onSurface, height: 1.4),
+ decoration: InputDecoration(hintText: '添加标题',
+ hintStyle: TextStyle(fontSize: isWin ? 22 : 16, fontWeight: FontWeight.w700, color: colors.onSurface.withValues(alpha: 0.2), height: 1.4),
+ border: InputBorder.none, enabledBorder: InputBorder.none, focusedBorder: InputBorder.none, isDense: true, contentPadding: EdgeInsets.zero),
+ onChanged: (_) => setState(() {})),
+ )),
),
- // Windows: 标签栏移到标题下方(靠左)
+ // Windows: 标签栏(彩色药丸样式)
if (isWin)
Container(
- padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
- decoration: BoxDecoration(border: Border(bottom: BorderSide(color: colors.outlineVariant, width: 0.5))),
- child: Align(
- alignment: Alignment.centerLeft,
- child: SingleChildScrollView(
- scrollDirection: Axis.horizontal,
- child: Row(mainAxisSize: MainAxisSize.min, children: [
- for (int i = 0; i < _editTags.length; i++)
- Padding(
- padding: const EdgeInsets.only(right: 4),
- child: Container(
- padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
- decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(6)),
- child: Row(mainAxisSize: MainAxisSize.min, children: [
- Text(_editTags[i], style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.6))),
- const SizedBox(width: 3),
- GestureDetector(
- onTap: () => setState(() => _editTags.removeAt(i)),
- child: Icon(Icons.close, size: 10, color: colors.onSurface.withValues(alpha: 0.3)),
+ padding: const EdgeInsets.symmetric(horizontal: 48, vertical: 8),
+ child: Center(child: ConstrainedBox(constraints: const BoxConstraints(maxWidth: 720),
+ child: Align(
+ alignment: Alignment.centerLeft,
+ child: SingleChildScrollView(
+ scrollDirection: Axis.horizontal,
+ child: Row(mainAxisSize: MainAxisSize.min, children: [
+ for (int i = 0; i < _editTags.length; i++)
+ Padding(
+ padding: const EdgeInsets.only(right: 6),
+ child: Container(
+ padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
+ decoration: BoxDecoration(
+ color: colors.primaryContainer,
+ borderRadius: BorderRadius.circular(12),
),
+ child: Row(mainAxisSize: MainAxisSize.min, children: [
+ Text(_editTags[i], style: TextStyle(fontSize: 12, color: colors.onPrimaryContainer, fontWeight: FontWeight.w500)),
+ const SizedBox(width: 4),
+ GestureDetector(
+ onTap: () => setState(() => _editTags.removeAt(i)),
+ child: Icon(Icons.close, size: 12, color: colors.onPrimaryContainer.withValues(alpha: 0.6)),
+ ),
+ ]),
+ ),
+ ),
+ GestureDetector(
+ onTap: _showEditTagPanel,
+ child: Container(
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5),
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(12),
+ border: Border.all(color: colors.outline.withValues(alpha: 0.3), width: 1),
+ ),
+ child: Row(mainAxisSize: MainAxisSize.min, children: [
+ Icon(Icons.add, size: 14, color: colors.onSurface.withValues(alpha: 0.4)),
+ const SizedBox(width: 3),
+ Text('标签', style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))),
]),
),
),
- GestureDetector(
- onTap: _showEditTagPanel,
- child: Container(
- padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
- decoration: BoxDecoration(
- borderRadius: BorderRadius.circular(6),
- border: Border.all(color: colors.onSurface.withValues(alpha: 0.25), width: 1),
- ),
- child: Row(mainAxisSize: MainAxisSize.min, children: [
- Icon(Icons.add, size: 12, color: colors.onSurface.withValues(alpha: 0.35)),
- const SizedBox(width: 2),
- Text('标签', style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.35))),
- ]),
- ),
- ),
- ]),
+ ]),
+ ),
),
- ),
+ )),
),
- // 内容编辑
+ // 内容编辑(Windows: 限宽居中)
Expanded(
child: isWin
- ? VditorEditor(
- key: _vditorKey,
- initialContent: _contentCtrl.text,
- noteId: widget.note.id,
- isDark: Theme.of(context).brightness == Brightness.dark,
- onContentChanged: (value) {
- _contentCtrl.text = value;
- _onContentChanged();
- },
- )
+ ? Center(child: ConstrainedBox(constraints: const BoxConstraints(maxWidth: 720),
+ child: VditorEditor(
+ key: _vditorKey,
+ initialContent: _contentCtrl.text,
+ noteId: widget.note.id,
+ isDark: Theme.of(context).brightness == Brightness.dark,
+ surfaceColor: colors.surface,
+ onContentChanged: (value) {
+ _contentCtrl.text = value;
+ _onContentChanged();
+ },
+ ),
+ ))
: TextField(controller: _contentCtrl, maxLines: null, expands: true,
textAlignVertical: TextAlignVertical.top,
strutStyle: const StrutStyle(forceStrutHeight: true, height: 1.6, fontSize: 14),
@@ -542,40 +579,63 @@ class _NoteDetailPageState extends State {
]),
]),
),
- // Windows: 底部只显示字数
+ // Windows: 底部字数(简洁)
if (isWin)
Container(
- padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
- decoration: BoxDecoration(border: Border(top: BorderSide(color: colors.outlineVariant, width: 0.5))),
- child: Row(mainAxisAlignment: MainAxisAlignment.end, children: [
- Text('${_contentCtrl.text.length} 字', style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.3))),
- ]),
+ padding: const EdgeInsets.symmetric(horizontal: 48, vertical: 8),
+ child: Center(child: ConstrainedBox(constraints: const BoxConstraints(maxWidth: 720),
+ child: Row(mainAxisAlignment: MainAxisAlignment.end, children: [
+ Text('${_contentCtrl.text.length} 字', style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.3))),
+ ]),
+ )),
),
]);
}
Widget _buildPreviewArea(ColorScheme colors, Note note) {
+ final isWin = Platform.isWindows;
return ListView(
- padding: const EdgeInsets.all(24),
+ padding: EdgeInsets.symmetric(vertical: isWin ? 32 : 24),
children: [
- if (_titleCtrl.text.isNotEmpty) ...[
- Text(_titleCtrl.text, style: TextStyle(fontSize: 24, fontWeight: FontWeight.w600, color: colors.onSurface, height: 1.3)),
- const SizedBox(height: 16),
- ],
- Markdown(
- data: _contentCtrl.text,
- styleSheet: _buildMarkdownStyleSheet(colors),
- padding: EdgeInsets.zero,
- shrinkWrap: true,
- physics: const NeverScrollableScrollPhysics(),
- // ignore: deprecated_member_use
- imageBuilder: (uri, title, alt) => _buildMarkdownImage(uri, note),
- ),
- if (_editImages.isNotEmpty) ...[
- const SizedBox(height: 16),
- _buildImageRow(_editImages),
- ],
- const SizedBox(height: 48),
+ Center(child: ConstrainedBox(constraints: const BoxConstraints(maxWidth: 720),
+ child: Padding(padding: EdgeInsets.symmetric(horizontal: isWin ? 48 : 24),
+ child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
+ if (_titleCtrl.text.isNotEmpty) ...[
+ Text(_titleCtrl.text, style: TextStyle(fontSize: isWin ? 28 : 24, fontWeight: FontWeight.w700, color: colors.onSurface, height: 1.3)),
+ const SizedBox(height: 12),
+ ],
+ // 标签
+ if (_editTags.isNotEmpty) ...[
+ Wrap(spacing: 6, runSpacing: 4, children: [
+ for (final tag in _editTags)
+ Container(
+ padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
+ decoration: BoxDecoration(
+ color: colors.primaryContainer,
+ borderRadius: BorderRadius.circular(12),
+ ),
+ child: Text(tag, style: TextStyle(fontSize: 12, color: colors.onPrimaryContainer, fontWeight: FontWeight.w500)),
+ ),
+ ]),
+ const SizedBox(height: 20),
+ ],
+ Markdown(
+ data: _contentCtrl.text,
+ styleSheet: _buildMarkdownStyleSheet(colors),
+ padding: EdgeInsets.zero,
+ shrinkWrap: true,
+ physics: const NeverScrollableScrollPhysics(),
+ // ignore: deprecated_member_use
+ imageBuilder: (uri, title, alt) => _buildMarkdownImage(uri, note),
+ ),
+ if (_editImages.isNotEmpty) ...[
+ const SizedBox(height: 16),
+ _buildImageRow(_editImages),
+ ],
+ const SizedBox(height: 48),
+ ]),
+ ),
+ )),
],
);
}
diff --git a/lib/utils/theme/app_theme.dart b/lib/utils/theme/app_theme.dart
index d27a581..15290b3 100644
--- a/lib/utils/theme/app_theme.dart
+++ b/lib/utils/theme/app_theme.dart
@@ -94,6 +94,17 @@ class AppTheme {
minLeadingWidth: 0, dense: true,
),
dividerTheme: DividerThemeData(color: scheme.outlineVariant, thickness: 0.5, space: 0),
+ scrollbarTheme: ScrollbarThemeData(
+ thumbColor: WidgetStateProperty.resolveWith((states) {
+ if (states.contains(WidgetState.hovered)) return scheme.onSurface.withValues(alpha: 0.3);
+ return scheme.onSurface.withValues(alpha: 0.1);
+ }),
+ thickness: WidgetStateProperty.resolveWith((states) {
+ if (states.contains(WidgetState.hovered)) return 6.0;
+ return 4.0;
+ }),
+ radius: const Radius.circular(3),
+ ),
inputDecorationTheme: InputDecorationTheme(
filled: false,
border: UnderlineInputBorder(borderSide: BorderSide(color: scheme.outlineVariant, width: 0.5)),
@@ -231,6 +242,17 @@ class AppTheme {
thickness: 0.5,
space: 0,
),
+ scrollbarTheme: ScrollbarThemeData(
+ thumbColor: WidgetStateProperty.resolveWith((states) {
+ if (states.contains(WidgetState.hovered)) return _gray.withValues(alpha: 0.3);
+ return _gray.withValues(alpha: 0.1);
+ }),
+ thickness: WidgetStateProperty.resolveWith((states) {
+ if (states.contains(WidgetState.hovered)) return 6.0;
+ return 4.0;
+ }),
+ radius: const Radius.circular(3),
+ ),
// 输入框 - 无边框,底部线
inputDecorationTheme: InputDecorationTheme(
@@ -438,6 +460,17 @@ class AppTheme {
thickness: 0.5,
space: 0,
),
+ scrollbarTheme: ScrollbarThemeData(
+ thumbColor: WidgetStateProperty.resolveWith((states) {
+ if (states.contains(WidgetState.hovered)) return _lightGray.withValues(alpha: 0.4);
+ return _lightGray.withValues(alpha: 0.15);
+ }),
+ thickness: WidgetStateProperty.resolveWith((states) {
+ if (states.contains(WidgetState.hovered)) return 6.0;
+ return 4.0;
+ }),
+ radius: const Radius.circular(3),
+ ),
inputDecorationTheme: InputDecorationTheme(
filled: false,
diff --git a/lib/utils/toast_util.dart b/lib/utils/toast_util.dart
index 1e9c304..a824bb4 100644
--- a/lib/utils/toast_util.dart
+++ b/lib/utils/toast_util.dart
@@ -1,4 +1,6 @@
+import 'dart:io';
import 'package:flutter/material.dart';
+import '../widgets/custom_title_bar.dart';
/// Toast 工具类
class ToastUtil {
@@ -13,7 +15,7 @@ class ToastUtil {
final overlay = Overlay.of(context);
_currentToast = OverlayEntry(
builder: (context) => Positioned(
- top: MediaQuery.of(context).padding.top + 80,
+ top: (Platform.isWindows ? CustomTitleBar.height : MediaQuery.of(context).padding.top) + 80,
left: 0,
right: 0,
child: Center(
diff --git a/lib/widgets/app_shell.dart b/lib/widgets/app_shell.dart
new file mode 100644
index 0000000..b318f4e
--- /dev/null
+++ b/lib/widgets/app_shell.dart
@@ -0,0 +1,19 @@
+import 'dart:io';
+import 'package:flutter/material.dart';
+import 'custom_title_bar.dart';
+
+class AppShell extends StatelessWidget {
+ final Widget child;
+ const AppShell({super.key, required this.child});
+
+ @override
+ Widget build(BuildContext context) {
+ if (!Platform.isWindows) return child;
+ return Column(
+ children: [
+ const CustomTitleBar(),
+ Expanded(child: child),
+ ],
+ );
+ }
+}
diff --git a/lib/widgets/custom_title_bar.dart b/lib/widgets/custom_title_bar.dart
new file mode 100644
index 0000000..8106a9f
--- /dev/null
+++ b/lib/widgets/custom_title_bar.dart
@@ -0,0 +1,148 @@
+import 'package:flutter/material.dart';
+import 'package:window_manager/window_manager.dart';
+
+class CustomTitleBar extends StatefulWidget {
+ const CustomTitleBar({super.key});
+
+ static const double height = 32.0;
+
+ @override
+ State createState() => CustomTitleBarState();
+}
+
+class CustomTitleBarState extends State with WindowListener {
+ bool _isMaximized = false;
+
+ @override
+ void initState() {
+ super.initState();
+ windowManager.addListener(this);
+ _checkMaximized();
+ }
+
+ Future _checkMaximized() async {
+ _isMaximized = await windowManager.isMaximized();
+ if (mounted) setState(() {});
+ }
+
+ @override
+ void onWindowMaximize() => setState(() => _isMaximized = true);
+
+ @override
+ void onWindowUnmaximize() => setState(() => _isMaximized = false);
+
+ @override
+ void dispose() {
+ windowManager.removeListener(this);
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final colors = Theme.of(context).colorScheme;
+
+ return GestureDetector(
+ onDoubleTap: () async {
+ if (await windowManager.isMaximized()) {
+ windowManager.unmaximize();
+ } else {
+ windowManager.maximize();
+ }
+ },
+ onPanStart: (_) => windowManager.startDragging(),
+ child: Container(
+ height: CustomTitleBar.height,
+ color: colors.surface,
+ child: Row(
+ children: [
+ const SizedBox(width: 12),
+ Image.asset('assets/icon/app_icon.webp', width: 16, height: 16),
+ const SizedBox(width: 8),
+ Text(
+ 'MookNote',
+ style: TextStyle(
+ fontSize: 12,
+ color: colors.onSurface.withValues(alpha: 0.7),
+ ),
+ ),
+ const Spacer(),
+ _WindowButton(
+ icon: Icons.remove,
+ size: 18,
+ onTap: () => windowManager.minimize(),
+ ),
+ _WindowButton(
+ icon: _isMaximized ? Icons.filter_none : Icons.crop_square,
+ size: 14,
+ onTap: () async {
+ if (await windowManager.isMaximized()) {
+ windowManager.unmaximize();
+ } else {
+ windowManager.maximize();
+ }
+ },
+ ),
+ _WindowButton(
+ icon: Icons.close,
+ size: 18,
+ onTap: () => windowManager.close(),
+ isClose: true,
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+class _WindowButton extends StatefulWidget {
+ final IconData icon;
+ final double size;
+ final VoidCallback onTap;
+ final bool isClose;
+
+ const _WindowButton({
+ required this.icon,
+ this.size = 16,
+ required this.onTap,
+ this.isClose = false,
+ });
+
+ @override
+ State<_WindowButton> createState() => _WindowButtonState();
+}
+
+class _WindowButtonState extends State<_WindowButton> {
+ bool _hovering = false;
+
+ @override
+ Widget build(BuildContext context) {
+ final colors = Theme.of(context).colorScheme;
+ Color bg;
+ Color fg;
+ if (widget.isClose && _hovering) {
+ bg = const Color(0xFFE81123);
+ fg = Colors.white;
+ } else if (_hovering) {
+ bg = colors.onSurface.withValues(alpha: 0.08);
+ fg = colors.onSurface;
+ } else {
+ bg = Colors.transparent;
+ fg = colors.onSurface.withValues(alpha: 0.7);
+ }
+
+ return GestureDetector(
+ onTap: widget.onTap,
+ child: MouseRegion(
+ onEnter: (_) => setState(() => _hovering = true),
+ onExit: (_) => setState(() => _hovering = false),
+ child: Container(
+ width: 46,
+ height: CustomTitleBar.height,
+ color: bg,
+ child: Icon(widget.icon, size: widget.size, color: fg),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/widgets/vditor_editor.dart b/lib/widgets/vditor_editor.dart
index 302c2d9..ebed552 100644
--- a/lib/widgets/vditor_editor.dart
+++ b/lib/widgets/vditor_editor.dart
@@ -12,6 +12,7 @@ class VditorEditor extends StatefulWidget {
final String? initialContent;
final String noteId;
final bool isDark;
+ final Color surfaceColor;
final ValueChanged? onContentChanged;
final String placeholder;
@@ -20,6 +21,7 @@ class VditorEditor extends StatefulWidget {
this.initialContent,
required this.noteId,
this.isDark = false,
+ this.surfaceColor = Colors.white,
this.onContentChanged,
this.placeholder = '使用 Markdown 格式书写...',
});
@@ -116,6 +118,14 @@ class VditorEditorState extends State {
} catch (_) {}
}
+ Future setBgColor(String hexColor) async {
+ if (_controller == null || !_isReady) return;
+ try {
+ final escaped = jsonEncode(hexColor);
+ await _controller!.evaluateJavascript(source: 'setBgColor($escaped)');
+ } catch (_) {}
+ }
+
Future insertValue(String text) async {
if (_controller == null || !_isReady) return;
try {
@@ -133,6 +143,20 @@ class VditorEditorState extends State {
if (widget.initialContent != null && widget.initialContent!.isNotEmpty) {
setValue(widget.initialContent!);
}
+ // 设置背景色
+ setBgColor(_colorToHex(widget.surfaceColor));
+ }
+
+ @override
+ void didUpdateWidget(covariant VditorEditor oldWidget) {
+ super.didUpdateWidget(oldWidget);
+ if (widget.surfaceColor != oldWidget.surfaceColor) {
+ setBgColor(_colorToHex(widget.surfaceColor));
+ }
+ }
+
+ static String _colorToHex(Color color) {
+ return '#${(color.value & 0xFFFFFF).toRadixString(16).padLeft(6, '0')}';
}
Future _pickImage() async {
diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc
index 36af2d7..c8f1f9e 100644
--- a/linux/flutter/generated_plugin_registrant.cc
+++ b/linux/flutter/generated_plugin_registrant.cc
@@ -8,7 +8,9 @@
#include
#include
+#include
#include
+#include
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) dynamic_color_registrar =
@@ -17,7 +19,13 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
+ g_autoptr(FlPluginRegistrar) screen_retriever_linux_registrar =
+ fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverLinuxPlugin");
+ screen_retriever_linux_plugin_register_with_registrar(screen_retriever_linux_registrar);
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
+ g_autoptr(FlPluginRegistrar) window_manager_registrar =
+ fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin");
+ window_manager_plugin_register_with_registrar(window_manager_registrar);
}
diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake
index eeaf357..eb72b7e 100644
--- a/linux/flutter/generated_plugins.cmake
+++ b/linux/flutter/generated_plugins.cmake
@@ -5,7 +5,9 @@
list(APPEND FLUTTER_PLUGIN_LIST
dynamic_color
file_selector_linux
+ screen_retriever_linux
url_launcher_linux
+ window_manager
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift
index 8080fcd..edc1b34 100644
--- a/macos/Flutter/GeneratedPluginRegistrant.swift
+++ b/macos/Flutter/GeneratedPluginRegistrant.swift
@@ -11,10 +11,12 @@ import file_picker
import file_selector_macos
import flutter_inappwebview_macos
import package_info_plus
+import screen_retriever_macos
import share_plus
import shared_preferences_foundation
import sqflite_darwin
import url_launcher_macos
+import window_manager
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
@@ -23,8 +25,10 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
+ ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin"))
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
+ WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin"))
}
diff --git a/pubspec.lock b/pubspec.lock
index 474bec1..6e2f760 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -757,6 +757,46 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.6.0"
+ screen_retriever:
+ dependency: transitive
+ description:
+ name: screen_retriever
+ sha256: ace919117a7520c13a50a6259e60c4a0d4cbe98809468792a91b5c5adada2aa6
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.2.2"
+ screen_retriever_linux:
+ dependency: transitive
+ description:
+ name: screen_retriever_linux
+ sha256: "7b52006a5ceae1f3d5af7f77188c3290d6e7d8ded16d99809bea84967c65c257"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.2.2"
+ screen_retriever_macos:
+ dependency: transitive
+ description:
+ name: screen_retriever_macos
+ sha256: a1489b99cce597c45a54b9aae1cd94c8d4705353b7e0bb2457a6e4de44e0ad8a
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.2.2"
+ screen_retriever_platform_interface:
+ dependency: transitive
+ description:
+ name: screen_retriever_platform_interface
+ sha256: "94a5535277510a63184ca178ce12a1449bc0b38618879aa1c18bf57369c5064a"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.2.2"
+ screen_retriever_windows:
+ dependency: transitive
+ description:
+ name: screen_retriever_windows
+ sha256: dafc6922b0bfbf1d48cf3ccbf519b4fff47bdcb820da1728ea6db675fecc9324
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.2.2"
share_plus:
dependency: "direct main"
description:
@@ -1066,6 +1106,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.0"
+ window_manager:
+ dependency: "direct main"
+ description:
+ name: window_manager
+ sha256: "732896e1416297c63c9e3fb95aea72d0355f61390263982a47fd519169dc5059"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.4.3"
xdg_directories:
dependency: transitive
description:
diff --git a/pubspec.yaml b/pubspec.yaml
index 7199385..03fb41c 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -36,6 +36,7 @@ dependencies:
sqflite_common_ffi: ^2.3.0
device_info_plus: ^11.2.0
crypto: ^3.0.6
+ window_manager: ^0.4.3
dev_dependencies:
flutter_test:
diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc
index ab82016..f2e40be 100644
--- a/windows/flutter/generated_plugin_registrant.cc
+++ b/windows/flutter/generated_plugin_registrant.cc
@@ -10,8 +10,10 @@
#include
#include
#include
+#include
#include
#include
+#include
void RegisterPlugins(flutter::PluginRegistry* registry) {
DynamicColorPluginCApiRegisterWithRegistrar(
@@ -22,8 +24,12 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("FlutterInappwebviewWindowsPluginCApi"));
PermissionHandlerWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
+ ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar(
+ registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi"));
SharePlusWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
+ WindowManagerPluginRegisterWithRegistrar(
+ registry->GetRegistrarForPlugin("WindowManagerPlugin"));
}
diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake
index ce48080..594ae95 100644
--- a/windows/flutter/generated_plugins.cmake
+++ b/windows/flutter/generated_plugins.cmake
@@ -7,8 +7,10 @@ list(APPEND FLUTTER_PLUGIN_LIST
file_selector_windows
flutter_inappwebview_windows
permission_handler_windows
+ screen_retriever_windows
share_plus
url_launcher_windows
+ window_manager
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST