优化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

@@ -13,6 +13,12 @@
.vditor-ir__marker--cursor, .vditor-ir__marker--cursor,
.vditor-content, .vditor-content,
.vditor-reset { background: transparent !important; } .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; }
</style> </style>
</head> </head>
<body> <body>
@@ -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) { function insertValue(text) {
if (vditor) vditor.insertValue(text); if (vditor) vditor.insertValue(text);
} }

View File

@@ -65,9 +65,12 @@ class NoteDao {
// 更新笔记 // 更新笔记
Future<int> updateNote(Note note) => _wrap('updateNote', () async { Future<int> updateNote(Note note) => _wrap('updateNote', () async {
final db = await _dbHelper.database; final db = await _dbHelper.database;
final data = note.toJson();
// 不覆盖 created_at保持创建时间不变
data.remove('created_at');
return await db.update( return await db.update(
'notes', 'notes',
note.toJson(), data,
where: 'id = ?', where: 'id = ?',
whereArgs: [note.id], whereArgs: [note.id],
); );

View File

@@ -10,6 +10,7 @@ import 'package:url_launcher/url_launcher.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:window_manager/window_manager.dart';
import 'pages/home/home_page.dart'; import 'pages/home/home_page.dart';
import 'utils/theme/app_theme.dart'; import 'utils/theme/app_theme.dart';
import 'utils/app_router.dart'; import 'utils/app_router.dart';
@@ -17,6 +18,7 @@ import 'utils/user_prefs.dart';
import 'services/changelog_service.dart'; import 'services/changelog_service.dart';
import 'services/usage_stats_service.dart'; import 'services/usage_stats_service.dart';
import 'providers/app_provider.dart'; import 'providers/app_provider.dart';
import 'widgets/app_shell.dart';
final RouteObserver<ModalRoute<void>> routeObserver = RouteObserver<ModalRoute<void>>(); final RouteObserver<ModalRoute<void>> routeObserver = RouteObserver<ModalRoute<void>>();
@@ -29,6 +31,13 @@ void main() async {
if (Platform.isWindows) { if (Platform.isWindows) {
sqfliteFfiInit(); sqfliteFfiInit();
databaseFactory = databaseFactoryFfi; 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 能拦截该协议的请求 // 注册 epub:// 自定义协议,使 WebView2 能拦截该协议的请求
try { try {
windowsWebViewEnvironment = await WebViewEnvironment.create(settings: windowsWebViewEnvironment = await WebViewEnvironment.create(settings:
@@ -243,6 +252,10 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
systemNavigationBarColor: isDark ? Colors.black : Colors.white, systemNavigationBarColor: isDark ? Colors.black : Colors.white,
systemNavigationBarIconBrightness: isDark ? Brightness.light : Brightness.dark, systemNavigationBarIconBrightness: isDark ? Brightness.light : Brightness.dark,
)); ));
// Windows: 同步窗口边框明暗
if (Platform.isWindows) {
windowManager.setBrightness(isDark ? Brightness.dark : Brightness.light);
}
} }
@override @override
@@ -305,6 +318,7 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
theme: light, theme: light,
darkTheme: dark, darkTheme: dark,
themeMode: provider.themeMode, themeMode: provider.themeMode,
builder: (ctx, nav) => AppShell(child: nav!),
localizationsDelegates: const [ localizationsDelegates: const [
GlobalMaterialLocalizations.delegate, GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate, GlobalWidgetsLocalizations.delegate,

View File

@@ -582,9 +582,8 @@ class _BookDetailPageState extends State<BookDetailPage> {
child: Row(children: [ child: Row(children: [
Icon(Icons.calendar_today_outlined, size: 14, color: colors.onSurface.withValues(alpha: 0.4)), Icon(Icons.calendar_today_outlined, size: 14, color: colors.onSurface.withValues(alpha: 0.4)),
const SizedBox(width: 8), const SizedBox(width: 8),
Text(hasDate ? '${date!.year}.${date!.month.toString().padLeft(2, '0')}.${date!.day.toString().padLeft(2, '0')}' : '选择日期', 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))), style: TextStyle(fontSize: 14, color: hasDate ? colors.onSurface : colors.onSurface.withValues(alpha: 0.25)), overflow: TextOverflow.ellipsis)),
const Spacer(),
if (clearable && hasDate) GestureDetector(onTap: () => onChanged(null), if (clearable && hasDate) GestureDetector(onTap: () => onChanged(null),
child: Icon(Icons.close, size: 14, color: colors.onSurface.withValues(alpha: 0.3))), child: Icon(Icons.close, size: 14, color: colors.onSurface.withValues(alpha: 0.3))),
]))), ]))),
@@ -743,9 +742,7 @@ class _BookDetailPageState extends State<BookDetailPage> {
FilledButton.tonalIcon( FilledButton.tonalIcon(
onPressed: () { onPressed: () {
if (Platform.isWindows) { if (Platform.isWindows) {
ScaffoldMessenger.of(context).showSnackBar( ToastUtil.show(context, 'Windows 桌面客户端暂不支持 EPUB 阅读功能');
const SnackBar(content: Text('Windows 桌面客户端暂不支持 EPUB 阅读功能')),
);
return; return;
} }
Navigator.push(context, MaterialPageRoute( Navigator.push(context, MaterialPageRoute(

View File

@@ -94,9 +94,7 @@ class _EpubDetailPageState extends State<EpubDetailPage> {
void _navigateToReader() { void _navigateToReader() {
if (Platform.isWindows) { if (Platform.isWindows) {
ScaffoldMessenger.of(context).showSnackBar( ToastUtil.show(context, 'Windows 桌面客户端暂不支持 EPUB 阅读功能');
const SnackBar(content: Text('Windows 桌面客户端暂不支持 EPUB 阅读功能')),
);
return; return;
} }
final coverPath = _linkedBookCoverPath ?? _book['cover_path'] as String?; final coverPath = _linkedBookCoverPath ?? _book['cover_path'] as String?;
@@ -150,9 +148,11 @@ class _EpubDetailPageState extends State<EpubDetailPage> {
final publisher = _book['publisher'] as String? ?? ''; final publisher = _book['publisher'] as String? ?? '';
final isbn = _book['isbn'] as String? ?? ''; final isbn = _book['isbn'] as String? ?? '';
final isWin = Platform.isWindows;
return Scaffold( return Scaffold(
backgroundColor: colors.surface, backgroundColor: colors.surface,
appBar: AppBar( appBar: isWin ? null : AppBar(
backgroundColor: colors.surface, backgroundColor: colors.surface,
elevation: 0, elevation: 0,
leading: IconButton( leading: IconButton(
@@ -167,151 +167,159 @@ class _EpubDetailPageState extends State<EpubDetailPage> {
const SizedBox(width: 4), const SizedBox(width: 4),
], ],
), ),
body: SingleChildScrollView( body: Column(children: [
child: Column( // Windows: 自定义顶栏
crossAxisAlignment: CrossAxisAlignment.start, if (isWin)
children: [ 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: [
// ── 封面 + 基本信息(横向布局)── // ── 封面 + 基本信息(横向布局)──
Padding( Row(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// 封面
GestureDetector( GestureDetector(
onTap: _navigateToReader, onTap: _navigateToReader,
child: SizedBox( child: SizedBox(width: 110, height: 154, child: _buildCover(coverPath, colors)),
width: 110, height: 154,
child: _buildCover(coverPath, colors),
),
), ),
const SizedBox(width: 16), const SizedBox(width: 16),
// 信息 Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, maxLines: 3, overflow: TextOverflow.ellipsis, Text(title, maxLines: 3, overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, style: TextStyle(fontSize: isWin ? 20 : 18, fontWeight: FontWeight.w700, color: colors.onSurface, height: 1.3)),
color: colors.onSurface, height: 1.3)),
if (author.isNotEmpty) ...[ if (author.isNotEmpty) ...[
const SizedBox(height: 4), const SizedBox(height: 4),
Text(author, maxLines: 1, overflow: TextOverflow.ellipsis, Text(author, maxLines: 1, overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.5))), style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.5))),
], ],
// 元数据标签
if (_bookInfo != null) ...[ if (_bookInfo != null) ...[
const SizedBox(height: 10), const SizedBox(height: 10),
Wrap(spacing: 6, runSpacing: 6, children: [ Wrap(spacing: 6, runSpacing: 4, children: [
_buildTag('${_bookInfo!.spine.length}', colors), Container(
_buildTag('EPUB ${_bookInfo!.epubVersion}', colors), 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(height: 12), const SizedBox(height: 12),
// 进度
Row(children: [ Row(children: [
Expanded( Expanded(child: ClipRRect(borderRadius: BorderRadius.circular(2),
child: ClipRRect( child: LinearProgressIndicator(value: progress > 0 ? progress : 0, minHeight: 3,
borderRadius: BorderRadius.circular(2), backgroundColor: colors.surfaceContainerHighest))),
child: LinearProgressIndicator(
value: progress > 0 ? progress : 0,
minHeight: 3,
backgroundColor: colors.surfaceContainerHighest,
),
),
),
const SizedBox(width: 8), const SizedBox(width: 8),
Text(progress > 0 ? '${(progress * 100).toInt()}%' : '未开始', Text(progress > 0 ? '${(progress * 100).toInt()}%' : '未开始',
style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))), style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))),
]), ]),
])),
], ],
), ),
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), Divider(height: 0.5, thickness: 0.5, color: colors.outline),
// ── 描述 ── // ── 描述 ──
if (description.isNotEmpty) ...[ if (description.isNotEmpty) ...[
_buildSectionHeader('简介', colors), _buildSectionHeader('\u7B80\u4ECB', colors),
Padding( Padding(padding: const EdgeInsets.fromLTRB(0, 0, 0, 12),
padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
child: GestureDetector( child: GestureDetector(
onTap: () => setState(() => _descriptionExpanded = !_descriptionExpanded), onTap: () => setState(() => _descriptionExpanded = !_descriptionExpanded),
child: Text(_stripHtmlTags(description), child: Text(_stripHtmlTags(description),
maxLines: _descriptionExpanded ? null : 4, maxLines: _descriptionExpanded ? null : 4,
overflow: _descriptionExpanded ? null : TextOverflow.ellipsis, overflow: _descriptionExpanded ? null : TextOverflow.ellipsis,
style: TextStyle(fontSize: 14, height: 1.7, color: colors.onSurface)), style: TextStyle(fontSize: 14, height: 1.7, color: colors.onSurface)),
), )),
),
Divider(height: 0.5, thickness: 0.5, color: colors.outline), Divider(height: 0.5, thickness: 0.5, color: colors.outline),
], ],
// ── 出版信息 ── // ── 出版信息 ──
if (publisher.isNotEmpty || isbn.isNotEmpty) ...[ if (publisher.isNotEmpty || isbn.isNotEmpty) ...[
_buildSectionHeader('出版信息', colors), _buildSectionHeader('\u51FA\u7248\u4FE1\u606F', colors),
Padding( Padding(padding: const EdgeInsets.fromLTRB(0, 0, 0, 12),
padding: const EdgeInsets.fromLTRB(16, 0, 16, 12), child: Wrap(spacing: 16, runSpacing: 6, children: [
child: Wrap( if (publisher.isNotEmpty) Row(mainAxisSize: MainAxisSize.min, children: [
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)), Icon(Icons.business_outlined, size: 14, color: colors.onSurface.withValues(alpha: 0.4)),
const SizedBox(width: 4), const SizedBox(width: 4),
Text(publisher, style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.7))), Text(publisher, style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.7))),
]), ]),
if (isbn.isNotEmpty) if (isbn.isNotEmpty) Row(mainAxisSize: MainAxisSize.min, children: [
Row(mainAxisSize: MainAxisSize.min, children: [
Icon(Icons.qr_code_outlined, size: 14, color: colors.onSurface.withValues(alpha: 0.4)), Icon(Icons.qr_code_outlined, size: 14, color: colors.onSurface.withValues(alpha: 0.4)),
const SizedBox(width: 4), const SizedBox(width: 4),
Text(isbn, style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.7))), 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), _buildSectionHeader('\u5173\u8054\u4E66\u7C4D', colors),
Padding( Padding(padding: const EdgeInsets.fromLTRB(0, 0, 0, 24),
padding: const EdgeInsets.fromLTRB(16, 0, 16, 24), child: _buildLinkedBookCard(colors)),
child: _buildLinkedBookCard(colors),
),
// ── 句读(高亮)── // ── 句读(高亮)──
Divider(height: 0.5, thickness: 0.5, color: colors.outline), Divider(height: 0.5, thickness: 0.5, color: colors.outline),
_buildHighlightsSectionHeader(colors), _buildHighlightsSectionHeader(colors),
Padding( Padding(padding: const EdgeInsets.fromLTRB(0, 0, 0, 24),
padding: const EdgeInsets.fromLTRB(0, 0, 0, 24), child: _buildHighlightsList(colors)),
child: _buildHighlightsList(colors),
),
// ── 书籍摘抄(仅关联书籍时显示)── // ── 书籍摘抄 ──
if ((_book['book_id'] as String? ?? '').isNotEmpty) ...[ if ((_book['book_id'] as String? ?? '').isNotEmpty) ...[
Divider(height: 0.5, thickness: 0.5, color: colors.outline), Divider(height: 0.5, thickness: 0.5, color: colors.outline),
_buildExcerptsSectionHeader(colors), _buildExcerptsSectionHeader(colors),
Padding( Padding(padding: const EdgeInsets.fromLTRB(0, 0, 0, 24),
padding: const EdgeInsets.fromLTRB(0, 0, 0, 24), child: _buildExcerptsList(colors)),
child: _buildExcerptsList(colors),
),
], ],
// ── 其他作品(同作者)── // ── 其他作品 ──
if (author.isNotEmpty) ...[ if (author.isNotEmpty) ...[
Divider(height: 0.5, thickness: 0.5, color: colors.outline), Divider(height: 0.5, thickness: 0.5, color: colors.outline),
_buildSectionHeader('其他作品', colors), _buildSectionHeader('\u5176\u4ED6\u4F5C\u54C1', colors),
_buildOtherWorks(author, colors), _buildOtherWorks(author, colors),
const SizedBox(height: 24), const SizedBox(height: 24),
], ],
], ]),
), ),
), )),
bottomNavigationBar: Container( )),
]),
// 非 Windows: 底部阅读按钮
bottomNavigationBar: isWin ? null : Container(
padding: EdgeInsets.fromLTRB(16, 12, 16, 12 + MediaQuery.of(context).padding.bottom), padding: EdgeInsets.fromLTRB(16, 12, 16, 12 + MediaQuery.of(context).padding.bottom),
decoration: BoxDecoration( decoration: BoxDecoration(
color: colors.surface, color: colors.surface,
@@ -328,7 +336,7 @@ class _EpubDetailPageState extends State<EpubDetailPage> {
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
), ),
child: Text( child: Text(
progress > 0 ? '继续阅读' : '开始阅读', progress > 0 ? '\u7EE7\u7EED\u9605\u8BFB' : '\u5F00\u59CB\u9605\u8BFB',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: colors.onPrimary), 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) { void _navigateToHighlight(Map<String, dynamic> highlight) {
if (Platform.isWindows) { if (Platform.isWindows) {
ScaffoldMessenger.of(context).showSnackBar( ToastUtil.show(context, 'Windows 桌面客户端暂不支持 EPUB 阅读功能');
const SnackBar(content: Text('Windows 桌面客户端暂不支持 EPUB 阅读功能')),
);
return; return;
} }
final chapter = int.tryParse(highlight['chapter'] as String? ?? '') ?? 0; 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 '../../services/epub/epub_service.dart';
import '../../utils/user_prefs.dart'; import '../../utils/user_prefs.dart';
import '../../utils/responsive.dart'; import '../../utils/responsive.dart';
import '../../utils/toast_util.dart';
import 'epub_detail_page.dart'; import 'epub_detail_page.dart';
import 'widgets/book_grid_item.dart'; import 'widgets/book_grid_item.dart';
@@ -84,9 +85,7 @@ class _EpubLibraryPageState extends State<EpubLibraryPage> {
if (path == null) return; if (path == null) return;
if (!path.toLowerCase().endsWith('.epub')) { if (!path.toLowerCase().endsWith('.epub')) {
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( ToastUtil.show(context, '\u4EC5\u652F\u6301\u5BFC\u5165 .epub \u683C\u5F0F\u7684\u6587\u4EF6');
const SnackBar(content: Text('仅支持导入 .epub 格式的文件')),
);
} }
return; return;
} }
@@ -105,9 +104,7 @@ class _EpubLibraryPageState extends State<EpubLibraryPage> {
if (imported != null) { if (imported != null) {
await _loadBooks(); await _loadBooks();
} else if (mounted) { } else if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( ToastUtil.show(context, 'EPUB \u89E3\u6790\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u6587\u4EF6');
const SnackBar(content: Text('EPUB 解析失败,请检查文件')),
);
} }
} }
@@ -227,9 +224,10 @@ class _EpubLibraryPageState extends State<EpubLibraryPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme; final colors = Theme.of(context).colorScheme;
final isWin = Platform.isWindows;
return Scaffold( return Scaffold(
backgroundColor: colors.surface, backgroundColor: colors.surface,
appBar: AppBar( appBar: isWin ? null : AppBar(
backgroundColor: colors.surface, backgroundColor: colors.surface,
elevation: 0, elevation: 0,
title: _isSearching title: _isSearching
@@ -250,7 +248,45 @@ class _EpubLibraryPageState extends State<EpubLibraryPage> {
icon: Icon(_isSearching ? Icons.close : Icons.arrow_back, size: 20), icon: Icon(_isSearching ? Icons.close : Icons.arrow_back, size: 20),
onPressed: _isSearching ? _toggleSearch : () => Navigator.pop(context), onPressed: _isSearching ? _toggleSearch : () => Navigator.pop(context),
), ),
actions: [ actions: _buildActions(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) if (!_isSearching)
IconButton( IconButton(
icon: Icon(Icons.search, size: 20, color: colors.onSurface.withValues(alpha: 0.6)), icon: Icon(Icons.search, size: 20, color: colors.onSurface.withValues(alpha: 0.6)),
@@ -275,16 +311,7 @@ class _EpubLibraryPageState extends State<EpubLibraryPage> {
onPressed: _pickAndImport, onPressed: _pickAndImport,
), ),
const SizedBox(width: 4), const SizedBox(width: 4),
], ];
),
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),
);
} }
Widget _buildEmpty(ColorScheme colors) { Widget _buildEmpty(ColorScheme colors) {

View File

@@ -324,7 +324,7 @@ class _DesktopIconRail extends StatelessWidget {
width: 160, width: 160,
child: Column( child: Column(
children: [ children: [
SizedBox(height: MediaQuery.of(context).padding.top + 8), SizedBox(height: (Platform.isWindows ? 0 : MediaQuery.of(context).padding.top) + 8),
// 头像 + 昵称 + 座右铭 // 头像 + 昵称 + 座右铭
_buildProfileHeader(context), _buildProfileHeader(context),
const SizedBox(height: 10), const SizedBox(height: 10),
@@ -3531,7 +3531,7 @@ class _SearchDialogState extends State<_SearchDialog> {
Widget _toggleBtn(String label, bool selected, VoidCallback onTap, ColorScheme colors) { Widget _toggleBtn(String label, bool selected, VoidCallback onTap, ColorScheme colors) {
return Material( return Material(
color: selected ? colors.onSurface : Colors.transparent, color: selected ? colors.primary : Colors.transparent,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
child: InkWell( child: InkWell(
onTap: onTap, onTap: onTap,
@@ -3544,7 +3544,7 @@ class _SearchDialogState extends State<_SearchDialog> {
Icon( Icon(
selected ? Icons.search_rounded : Icons.search_outlined, selected ? Icons.search_rounded : Icons.search_outlined,
size: 14, 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), const SizedBox(width: 5),
Text( Text(
@@ -3552,7 +3552,7 @@ class _SearchDialogState extends State<_SearchDialog> {
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
fontWeight: selected ? FontWeight.w600 : FontWeight.w400, 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( return Column(
children: [ children: [
// 顶部搜索栏 // 顶部搜索栏
SizedBox(height: MediaQuery.of(context).padding.top), SizedBox(height: Platform.isWindows ? 0 : MediaQuery.of(context).padding.top),
Padding( Padding(
padding: const EdgeInsets.fromLTRB(12, 8, 12, 4), padding: const EdgeInsets.fromLTRB(12, 8, 12, 4),
child: Row( child: Row(
@@ -3939,21 +3939,20 @@ class _DesktopListPanelState extends State<_DesktopListPanel> {
Widget _buildNoteList(BuildContext context) { Widget _buildNoteList(BuildContext context) {
return Consumer<AppProvider>( return Consumer<AppProvider>(
builder: (context, provider, _) { 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); if (items.isEmpty) return _buildEmpty('暂无笔记记录', Icons.note_outlined);
return ListView.builder( return ListView.builder(
padding: const EdgeInsets.symmetric(vertical: 4), padding: const EdgeInsets.symmetric(vertical: 4),
itemCount: items.length, itemCount: items.length,
itemBuilder: (_, i) => _CompactListItem( itemBuilder: (_, i) => _DesktopNoteItem(
title: items[i].title.isNotEmpty ? items[i].title : '随手记', note: items[i],
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,
selected: provider.selectedNote?.id == items[i].id, selected: provider.selectedNote?.id == items[i].id,
onTap: () { onTap: () => provider.selectNote(items[i]),
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<AppProvider>();
final overlay = Overlay.of(context);
final renderBox = context.findRenderObject() as RenderBox;
final position = renderBox.localToGlobal(details.localPosition);
showMenu<String>(
context: context,
position: RelativeRect.fromLTRB(position.dx, position.dy, position.dx + 1, position.dy + 1),
items: [
PopupMenuItem<String>(
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<String>(
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<String>(
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<bool>(
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 { class _SearchGroupHeader extends StatelessWidget {

View File

@@ -57,19 +57,20 @@ class _NoteAddPageState extends State<NoteAddPage> {
children: [ children: [
// 顶栏 // 顶栏
Container( Container(
height: 48, height: 52,
decoration: BoxDecoration( decoration: BoxDecoration(
color: colors.surface, color: colors.surface,
border: Border(bottom: BorderSide(color: colors.outlineVariant, width: 0.5)), border: Border(bottom: BorderSide(color: colors.outlineVariant, width: 0.5)),
), ),
child: Row(children: [ child: Row(children: [
const SizedBox(width: 8),
IconButton( IconButton(
icon: Icon(Icons.close, color: colors.onSurface, size: 18), icon: Icon(Icons.close, color: colors.onSurface, size: 18),
onPressed: () => widget.onCancel?.call(), onPressed: () => widget.onCancel?.call(),
), ),
Expanded( Expanded(
child: Text('添加笔记', 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( Container(
@@ -80,7 +81,7 @@ class _NoteAddPageState extends State<NoteAddPage> {
_editModeChip(Icons.visibility_outlined, '预览', 'preview', colors), _editModeChip(Icons.visibility_outlined, '预览', 'preview', colors),
]), ]),
), ),
const SizedBox(width: 8), const SizedBox(width: 12),
FilledButton.icon( FilledButton.icon(
onPressed: _save, onPressed: _save,
icon: const Icon(Icons.check, size: 16), icon: const Icon(Icons.check, size: 16),
@@ -90,7 +91,7 @@ class _NoteAddPageState extends State<NoteAddPage> {
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
), ),
), ),
const SizedBox(width: 12), const SizedBox(width: 16),
]), ]),
), ),
// 主体 // 主体
@@ -126,29 +127,31 @@ class _NoteAddPageState extends State<NoteAddPage> {
Widget _buildEditArea(ColorScheme colors) { Widget _buildEditArea(ColorScheme colors) {
final isWin = Platform.isWindows; final isWin = Platform.isWindows;
return Column(children: [ return Column(children: [
// 标题输入 // 标题输入Windows: 更大更醒目)
Container( 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))), decoration: BoxDecoration(border: Border(bottom: BorderSide(color: colors.outlineVariant, width: 0.5))),
child: Center(child: ConstrainedBox(constraints: const BoxConstraints(maxWidth: 720),
child: TextField( child: TextField(
controller: _titleCtrl, controller: _titleCtrl,
maxLines: 1, maxLines: 1,
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: colors.onSurface), style: TextStyle(fontSize: isWin ? 22 : 16, fontWeight: FontWeight.w700, color: colors.onSurface, height: 1.4),
decoration: InputDecoration( decoration: InputDecoration(
hintText: '添加标题', hintText: '添加标题',
hintStyle: TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: colors.onSurface.withValues(alpha: 0.2)), 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, border: InputBorder.none, enabledBorder: InputBorder.none, focusedBorder: InputBorder.none,
isDense: true, isDense: true,
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
), ),
onChanged: (_) => setState(() {}), onChanged: (_) => setState(() {}),
), ),
)),
), ),
// Windows: 标签栏移到标题下方(靠左 // Windows: 标签栏(彩色药丸样式
if (isWin) if (isWin)
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 48, vertical: 8),
decoration: BoxDecoration(border: Border(bottom: BorderSide(color: colors.outlineVariant, width: 0.5))), child: Center(child: ConstrainedBox(constraints: const BoxConstraints(maxWidth: 720),
child: Align( child: Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: SingleChildScrollView( child: SingleChildScrollView(
@@ -156,16 +159,19 @@ class _NoteAddPageState extends State<NoteAddPage> {
child: Row(mainAxisSize: MainAxisSize.min, children: [ child: Row(mainAxisSize: MainAxisSize.min, children: [
for (int i = 0; i < _tags.length; i++) for (int i = 0; i < _tags.length; i++)
Padding( Padding(
padding: const EdgeInsets.only(right: 4), padding: const EdgeInsets.only(right: 6),
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(6)), decoration: BoxDecoration(
color: colors.primaryContainer,
borderRadius: BorderRadius.circular(12),
),
child: Row(mainAxisSize: MainAxisSize.min, children: [ child: Row(mainAxisSize: MainAxisSize.min, children: [
Text(_tags[i], style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.6))), Text(_tags[i], style: TextStyle(fontSize: 12, color: colors.onPrimaryContainer, fontWeight: FontWeight.w500)),
const SizedBox(width: 3), const SizedBox(width: 4),
GestureDetector( GestureDetector(
onTap: () => setState(() => _tags.removeAt(i)), onTap: () => setState(() => _tags.removeAt(i)),
child: Icon(Icons.close, size: 10, color: colors.onSurface.withValues(alpha: 0.3)), child: Icon(Icons.close, size: 12, color: colors.onPrimaryContainer.withValues(alpha: 0.6)),
), ),
]), ]),
), ),
@@ -173,35 +179,39 @@ class _NoteAddPageState extends State<NoteAddPage> {
GestureDetector( GestureDetector(
onTap: _showTagPanel, onTap: _showTagPanel,
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(12),
border: Border.all(color: colors.onSurface.withValues(alpha: 0.25), width: 1), border: Border.all(color: colors.outline.withValues(alpha: 0.3), width: 1),
), ),
child: Row(mainAxisSize: MainAxisSize.min, children: [ child: Row(mainAxisSize: MainAxisSize.min, children: [
Icon(Icons.add, size: 12, color: colors.onSurface.withValues(alpha: 0.35)), Icon(Icons.add, size: 14, color: colors.onSurface.withValues(alpha: 0.4)),
const SizedBox(width: 2), const SizedBox(width: 3),
Text('标签', style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.35))), Text('标签', style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))),
]), ]),
), ),
), ),
]), ]),
), ),
), ),
)),
), ),
// 内容编辑 // 内容编辑Windows: 限宽居中)
Expanded( Expanded(
child: isWin child: isWin
? VditorEditor( ? Center(child: ConstrainedBox(constraints: const BoxConstraints(maxWidth: 720),
child: VditorEditor(
key: _vditorKey, key: _vditorKey,
initialContent: _contentCtrl.text, initialContent: _contentCtrl.text,
noteId: _tempId, noteId: _tempId,
isDark: Theme.of(context).brightness == Brightness.dark, isDark: Theme.of(context).brightness == Brightness.dark,
surfaceColor: colors.surface,
onContentChanged: (value) { onContentChanged: (value) {
_contentCtrl.text = value; _contentCtrl.text = value;
setState(() {}); setState(() {});
}, },
) ),
))
: TextField( : TextField(
controller: _contentCtrl, controller: _contentCtrl,
maxLines: null, maxLines: null,
@@ -284,14 +294,15 @@ class _NoteAddPageState extends State<NoteAddPage> {
]), ]),
]), ]),
), ),
// Windows: 底部只显示字数 // Windows: 底部字数(简洁)
if (isWin) if (isWin)
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), padding: const EdgeInsets.symmetric(horizontal: 48, vertical: 8),
decoration: BoxDecoration(border: Border(top: BorderSide(color: colors.outlineVariant, width: 0.5))), child: Center(child: ConstrainedBox(constraints: const BoxConstraints(maxWidth: 720),
child: Row(mainAxisAlignment: MainAxisAlignment.end, children: [ child: Row(mainAxisAlignment: MainAxisAlignment.end, children: [
Text('${_contentCtrl.text.length}', style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.3))), Text('${_contentCtrl.text.length}', style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.3))),
]), ]),
)),
), ),
]); ]);
} }
@@ -526,9 +537,12 @@ class _NoteAddPageState extends State<NoteAddPage> {
Future<void> _save() async { Future<void> _save() async {
final title = _titleCtrl.text.trim(); final title = _titleCtrl.text.trim();
final content = Platform.isWindows String content;
? (await _vditorKey.currentState?.getValue() ?? '').trim() if (Platform.isWindows && _vditorKey.currentState != null && _vditorKey.currentState!.isReady) {
: _contentCtrl.text.trim(); content = (await _vditorKey.currentState!.getValue()).trim();
} else {
content = _contentCtrl.text.trim();
}
if (title.isEmpty && content.isEmpty) { if (title.isEmpty && content.isEmpty) {
ToastUtil.show(context, '标题或内容不能为空'); ToastUtil.show(context, '标题或内容不能为空');
return; return;

View File

@@ -78,9 +78,12 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
} }
Future<void> _autoSave() async { Future<void> _autoSave() async {
final content = Platform.isWindows String content;
? (await _vditorKey.currentState?.getValue() ?? '').trim() if (Platform.isWindows && _vditorKey.currentState != null && _vditorKey.currentState!.isReady) {
: _contentCtrl.text.trim(); content = (await _vditorKey.currentState!.getValue()).trim();
} else {
content = _contentCtrl.text.trim();
}
final title = _titleCtrl.text.trim(); final title = _titleCtrl.text.trim();
if (title.isEmpty && content.isEmpty) return; if (title.isEmpty && content.isEmpty) return;
try { try {
@@ -103,9 +106,12 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
Future<void> _saveEdit() async { Future<void> _saveEdit() async {
_autoSaveTimer?.cancel(); _autoSaveTimer?.cancel();
final title = _titleCtrl.text.trim(); final title = _titleCtrl.text.trim();
final content = Platform.isWindows String content;
? (await _vditorKey.currentState?.getValue() ?? '').trim() if (Platform.isWindows && _vditorKey.currentState != null && _vditorKey.currentState!.isReady) {
: _contentCtrl.text.trim(); content = (await _vditorKey.currentState!.getValue()).trim();
} else {
content = _contentCtrl.text.trim();
}
if (title.isEmpty && content.isEmpty) { if (title.isEmpty && content.isEmpty) {
ToastUtil.show(context, '标题或内容不能为空'); ToastUtil.show(context, '标题或内容不能为空');
return; return;
@@ -241,12 +247,13 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
children: [ children: [
// 顶栏 // 顶栏
Container( Container(
height: 48, height: 52,
decoration: BoxDecoration( decoration: BoxDecoration(
color: colors.surface, color: colors.surface,
border: Border(bottom: BorderSide(color: colors.outlineVariant, width: 0.5)), border: Border(bottom: BorderSide(color: colors.outlineVariant, width: 0.5)),
), ),
child: Row(children: [ child: Row(children: [
const SizedBox(width: 8),
IconButton( IconButton(
icon: Icon(Icons.arrow_back, color: colors.onSurface, size: 18), icon: Icon(Icons.arrow_back, color: colors.onSurface, size: 18),
onPressed: widget.embedded onPressed: widget.embedded
@@ -256,55 +263,75 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
Expanded( Expanded(
child: Text( child: Text(
note.title.isNotEmpty ? note.title : _truncateContent(note.content), 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), 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(
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))),
],
),
),
if (note.tags.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 6, left: 24, right: 24),
child: _buildTagRow(note.tags),
),
// 内容
Expanded( Expanded(
child: Markdown( child: ListView(
padding: const EdgeInsets.symmetric(vertical: 32),
children: [
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, data: note.content,
styleSheet: _buildMarkdownStyleSheet(colors), styleSheet: _buildMarkdownStyleSheet(colors),
padding: const EdgeInsets.all(24), padding: EdgeInsets.zero,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
// ignore: deprecated_member_use // ignore: deprecated_member_use
imageBuilder: (uri, title, alt) => _buildMarkdownImage(uri, note), imageBuilder: (uri, title, alt) => _buildMarkdownImage(uri, note),
), ),
if (note.images.isNotEmpty) ...[
const SizedBox(height: 16),
_buildImageRow(note.images),
],
const SizedBox(height: 48),
]),
),
)),
],
),
), ),
if (note.images.isNotEmpty) _buildImageRow(note.images),
// 底部操作栏 // 底部操作栏
Container( Container(
height: 56, height: 52,
decoration: BoxDecoration( decoration: BoxDecoration(
color: colors.surface, color: colors.surface,
border: Border(top: BorderSide(color: colors.outlineVariant, width: 0.5)), border: Border(top: BorderSide(color: colors.outlineVariant, width: 0.5)),
@@ -348,14 +375,15 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
children: [ children: [
// 顶栏 // 顶栏
Container( Container(
height: 48, height: 52,
decoration: BoxDecoration(color: colors.surface, decoration: BoxDecoration(color: colors.surface,
border: Border(bottom: BorderSide(color: colors.outlineVariant, width: 0.5))), border: Border(bottom: BorderSide(color: colors.outlineVariant, width: 0.5))),
child: Row(children: [ child: Row(children: [
const SizedBox(width: 8),
IconButton(icon: Icon(Icons.close, color: colors.onSurface, size: 18), IconButton(icon: Icon(Icons.close, color: colors.onSurface, size: 18),
onPressed: () { _autoSaveTimer?.cancel(); if (_saveStatus == 'saved') _autoSave(); setState(() => _isEditing = false); }), onPressed: () { _autoSaveTimer?.cancel(); if (_saveStatus == 'saved') _autoSave(); setState(() => _isEditing = false); }),
Expanded(child: Text(_titleCtrl.text.isNotEmpty ? _titleCtrl.text : '编辑笔记', 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)), maxLines: 1, overflow: TextOverflow.ellipsis)),
// 编辑/预览切换 // 编辑/预览切换
Container( Container(
@@ -366,7 +394,7 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
_editModeChip(Icons.visibility_outlined, '预览', 'preview', colors), _editModeChip(Icons.visibility_outlined, '预览', 'preview', colors),
]), ]),
), ),
const SizedBox(width: 8), const SizedBox(width: 12),
if (_saveStatus == 'saved') if (_saveStatus == 'saved')
Padding(padding: const EdgeInsets.only(right: 8), Padding(padding: const EdgeInsets.only(right: 8),
child: Row(mainAxisSize: MainAxisSize.min, children: [ child: Row(mainAxisSize: MainAxisSize.min, children: [
@@ -378,7 +406,7 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
icon: const Icon(Icons.check, size: 16), label: const Text('保存'), icon: const Icon(Icons.check, size: 16), label: const Text('保存'),
style: FilledButton.styleFrom(padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), style: FilledButton.styleFrom(padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)))), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)))),
const SizedBox(width: 12), const SizedBox(width: 16),
]), ]),
), ),
// 主体 // 主体
@@ -409,22 +437,24 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
Widget _buildEditArea(ColorScheme colors, Note note) { Widget _buildEditArea(ColorScheme colors, Note note) {
final isWin = Platform.isWindows; final isWin = Platform.isWindows;
return Column(children: [ return Column(children: [
// 标题输入 // 标题输入Windows: 更大更醒目)
Container( 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))), decoration: BoxDecoration(border: Border(bottom: BorderSide(color: colors.outlineVariant, width: 0.5))),
child: Center(child: ConstrainedBox(constraints: const BoxConstraints(maxWidth: 720),
child: TextField(controller: _titleCtrl, maxLines: 1, child: TextField(controller: _titleCtrl, maxLines: 1,
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: colors.onSurface), style: TextStyle(fontSize: isWin ? 22 : 16, fontWeight: FontWeight.w700, color: colors.onSurface, height: 1.4),
decoration: InputDecoration(hintText: '添加标题', decoration: InputDecoration(hintText: '添加标题',
hintStyle: TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: colors.onSurface.withValues(alpha: 0.2)), 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), border: InputBorder.none, enabledBorder: InputBorder.none, focusedBorder: InputBorder.none, isDense: true, contentPadding: EdgeInsets.zero),
onChanged: (_) => setState(() {})), onChanged: (_) => setState(() {})),
)),
), ),
// Windows: 标签栏移到标题下方(靠左 // Windows: 标签栏(彩色药丸样式
if (isWin) if (isWin)
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 48, vertical: 8),
decoration: BoxDecoration(border: Border(bottom: BorderSide(color: colors.outlineVariant, width: 0.5))), child: Center(child: ConstrainedBox(constraints: const BoxConstraints(maxWidth: 720),
child: Align( child: Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: SingleChildScrollView( child: SingleChildScrollView(
@@ -432,16 +462,19 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
child: Row(mainAxisSize: MainAxisSize.min, children: [ child: Row(mainAxisSize: MainAxisSize.min, children: [
for (int i = 0; i < _editTags.length; i++) for (int i = 0; i < _editTags.length; i++)
Padding( Padding(
padding: const EdgeInsets.only(right: 4), padding: const EdgeInsets.only(right: 6),
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(6)), decoration: BoxDecoration(
color: colors.primaryContainer,
borderRadius: BorderRadius.circular(12),
),
child: Row(mainAxisSize: MainAxisSize.min, children: [ child: Row(mainAxisSize: MainAxisSize.min, children: [
Text(_editTags[i], style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.6))), Text(_editTags[i], style: TextStyle(fontSize: 12, color: colors.onPrimaryContainer, fontWeight: FontWeight.w500)),
const SizedBox(width: 3), const SizedBox(width: 4),
GestureDetector( GestureDetector(
onTap: () => setState(() => _editTags.removeAt(i)), onTap: () => setState(() => _editTags.removeAt(i)),
child: Icon(Icons.close, size: 10, color: colors.onSurface.withValues(alpha: 0.3)), child: Icon(Icons.close, size: 12, color: colors.onPrimaryContainer.withValues(alpha: 0.6)),
), ),
]), ]),
), ),
@@ -449,35 +482,39 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
GestureDetector( GestureDetector(
onTap: _showEditTagPanel, onTap: _showEditTagPanel,
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(12),
border: Border.all(color: colors.onSurface.withValues(alpha: 0.25), width: 1), border: Border.all(color: colors.outline.withValues(alpha: 0.3), width: 1),
), ),
child: Row(mainAxisSize: MainAxisSize.min, children: [ child: Row(mainAxisSize: MainAxisSize.min, children: [
Icon(Icons.add, size: 12, color: colors.onSurface.withValues(alpha: 0.35)), Icon(Icons.add, size: 14, color: colors.onSurface.withValues(alpha: 0.4)),
const SizedBox(width: 2), const SizedBox(width: 3),
Text('标签', style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.35))), Text('标签', style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.4))),
]), ]),
), ),
), ),
]), ]),
), ),
), ),
)),
), ),
// 内容编辑 // 内容编辑Windows: 限宽居中)
Expanded( Expanded(
child: isWin child: isWin
? VditorEditor( ? Center(child: ConstrainedBox(constraints: const BoxConstraints(maxWidth: 720),
child: VditorEditor(
key: _vditorKey, key: _vditorKey,
initialContent: _contentCtrl.text, initialContent: _contentCtrl.text,
noteId: widget.note.id, noteId: widget.note.id,
isDark: Theme.of(context).brightness == Brightness.dark, isDark: Theme.of(context).brightness == Brightness.dark,
surfaceColor: colors.surface,
onContentChanged: (value) { onContentChanged: (value) {
_contentCtrl.text = value; _contentCtrl.text = value;
_onContentChanged(); _onContentChanged();
}, },
) ),
))
: TextField(controller: _contentCtrl, maxLines: null, expands: true, : TextField(controller: _contentCtrl, maxLines: null, expands: true,
textAlignVertical: TextAlignVertical.top, textAlignVertical: TextAlignVertical.top,
strutStyle: const StrutStyle(forceStrutHeight: true, height: 1.6, fontSize: 14), strutStyle: const StrutStyle(forceStrutHeight: true, height: 1.6, fontSize: 14),
@@ -542,25 +579,45 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
]), ]),
]), ]),
), ),
// Windows: 底部只显示字数 // Windows: 底部字数(简洁)
if (isWin) if (isWin)
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), padding: const EdgeInsets.symmetric(horizontal: 48, vertical: 8),
decoration: BoxDecoration(border: Border(top: BorderSide(color: colors.outlineVariant, width: 0.5))), child: Center(child: ConstrainedBox(constraints: const BoxConstraints(maxWidth: 720),
child: Row(mainAxisAlignment: MainAxisAlignment.end, children: [ child: Row(mainAxisAlignment: MainAxisAlignment.end, children: [
Text('${_contentCtrl.text.length}', style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.3))), Text('${_contentCtrl.text.length}', style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.3))),
]), ]),
)),
), ),
]); ]);
} }
Widget _buildPreviewArea(ColorScheme colors, Note note) { Widget _buildPreviewArea(ColorScheme colors, Note note) {
final isWin = Platform.isWindows;
return ListView( return ListView(
padding: const EdgeInsets.all(24), padding: EdgeInsets.symmetric(vertical: isWin ? 32 : 24),
children: [ children: [
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) ...[ if (_titleCtrl.text.isNotEmpty) ...[
Text(_titleCtrl.text, style: TextStyle(fontSize: 24, fontWeight: FontWeight.w600, color: colors.onSurface, height: 1.3)), Text(_titleCtrl.text, style: TextStyle(fontSize: isWin ? 28 : 24, fontWeight: FontWeight.w700, color: colors.onSurface, height: 1.3)),
const SizedBox(height: 16), 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( Markdown(
data: _contentCtrl.text, data: _contentCtrl.text,
@@ -576,6 +633,9 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
_buildImageRow(_editImages), _buildImageRow(_editImages),
], ],
const SizedBox(height: 48), const SizedBox(height: 48),
]),
),
)),
], ],
); );
} }

View File

@@ -94,6 +94,17 @@ class AppTheme {
minLeadingWidth: 0, dense: true, minLeadingWidth: 0, dense: true,
), ),
dividerTheme: DividerThemeData(color: scheme.outlineVariant, thickness: 0.5, space: 0), 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( inputDecorationTheme: InputDecorationTheme(
filled: false, filled: false,
border: UnderlineInputBorder(borderSide: BorderSide(color: scheme.outlineVariant, width: 0.5)), border: UnderlineInputBorder(borderSide: BorderSide(color: scheme.outlineVariant, width: 0.5)),
@@ -231,6 +242,17 @@ class AppTheme {
thickness: 0.5, thickness: 0.5,
space: 0, 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( inputDecorationTheme: InputDecorationTheme(
@@ -438,6 +460,17 @@ class AppTheme {
thickness: 0.5, thickness: 0.5,
space: 0, 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( inputDecorationTheme: InputDecorationTheme(
filled: false, filled: false,

View File

@@ -1,4 +1,6 @@
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../widgets/custom_title_bar.dart';
/// Toast 工具类 /// Toast 工具类
class ToastUtil { class ToastUtil {
@@ -13,7 +15,7 @@ class ToastUtil {
final overlay = Overlay.of(context); final overlay = Overlay.of(context);
_currentToast = OverlayEntry( _currentToast = OverlayEntry(
builder: (context) => Positioned( builder: (context) => Positioned(
top: MediaQuery.of(context).padding.top + 80, top: (Platform.isWindows ? CustomTitleBar.height : MediaQuery.of(context).padding.top) + 80,
left: 0, left: 0,
right: 0, right: 0,
child: Center( child: Center(

View File

@@ -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),
],
);
}
}

View File

@@ -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<CustomTitleBar> createState() => CustomTitleBarState();
}
class CustomTitleBarState extends State<CustomTitleBar> with WindowListener {
bool _isMaximized = false;
@override
void initState() {
super.initState();
windowManager.addListener(this);
_checkMaximized();
}
Future<void> _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),
),
),
);
}
}

View File

@@ -12,6 +12,7 @@ class VditorEditor extends StatefulWidget {
final String? initialContent; final String? initialContent;
final String noteId; final String noteId;
final bool isDark; final bool isDark;
final Color surfaceColor;
final ValueChanged<String>? onContentChanged; final ValueChanged<String>? onContentChanged;
final String placeholder; final String placeholder;
@@ -20,6 +21,7 @@ class VditorEditor extends StatefulWidget {
this.initialContent, this.initialContent,
required this.noteId, required this.noteId,
this.isDark = false, this.isDark = false,
this.surfaceColor = Colors.white,
this.onContentChanged, this.onContentChanged,
this.placeholder = '使用 Markdown 格式书写...', this.placeholder = '使用 Markdown 格式书写...',
}); });
@@ -116,6 +118,14 @@ class VditorEditorState extends State<VditorEditor> {
} catch (_) {} } catch (_) {}
} }
Future<void> setBgColor(String hexColor) async {
if (_controller == null || !_isReady) return;
try {
final escaped = jsonEncode(hexColor);
await _controller!.evaluateJavascript(source: 'setBgColor($escaped)');
} catch (_) {}
}
Future<void> insertValue(String text) async { Future<void> insertValue(String text) async {
if (_controller == null || !_isReady) return; if (_controller == null || !_isReady) return;
try { try {
@@ -133,6 +143,20 @@ class VditorEditorState extends State<VditorEditor> {
if (widget.initialContent != null && widget.initialContent!.isNotEmpty) { if (widget.initialContent != null && widget.initialContent!.isNotEmpty) {
setValue(widget.initialContent!); 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<void> _pickImage() async { Future<void> _pickImage() async {

View File

@@ -8,7 +8,9 @@
#include <dynamic_color/dynamic_color_plugin.h> #include <dynamic_color/dynamic_color_plugin.h>
#include <file_selector_linux/file_selector_plugin.h> #include <file_selector_linux/file_selector_plugin.h>
#include <screen_retriever_linux/screen_retriever_linux_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h> #include <url_launcher_linux/url_launcher_plugin.h>
#include <window_manager/window_manager_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) { void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) dynamic_color_registrar = g_autoptr(FlPluginRegistrar) dynamic_color_registrar =
@@ -17,7 +19,13 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
file_selector_plugin_register_with_registrar(file_selector_linux_registrar); 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 = g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); 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);
} }

View File

@@ -5,7 +5,9 @@
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
dynamic_color dynamic_color
file_selector_linux file_selector_linux
screen_retriever_linux
url_launcher_linux url_launcher_linux
window_manager
) )
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST

View File

@@ -11,10 +11,12 @@ import file_picker
import file_selector_macos import file_selector_macos
import flutter_inappwebview_macos import flutter_inappwebview_macos
import package_info_plus import package_info_plus
import screen_retriever_macos
import share_plus import share_plus
import shared_preferences_foundation import shared_preferences_foundation
import sqflite_darwin import sqflite_darwin
import url_launcher_macos import url_launcher_macos
import window_manager
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
@@ -23,8 +25,10 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin"))
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin"))
} }

View File

@@ -757,6 +757,46 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.6.0" 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: share_plus:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -1066,6 +1106,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.0" 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: xdg_directories:
dependency: transitive dependency: transitive
description: description:

View File

@@ -36,6 +36,7 @@ dependencies:
sqflite_common_ffi: ^2.3.0 sqflite_common_ffi: ^2.3.0
device_info_plus: ^11.2.0 device_info_plus: ^11.2.0
crypto: ^3.0.6 crypto: ^3.0.6
window_manager: ^0.4.3
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View File

@@ -10,8 +10,10 @@
#include <file_selector_windows/file_selector_windows.h> #include <file_selector_windows/file_selector_windows.h>
#include <flutter_inappwebview_windows/flutter_inappwebview_windows_plugin_c_api.h> #include <flutter_inappwebview_windows/flutter_inappwebview_windows_plugin_c_api.h>
#include <permission_handler_windows/permission_handler_windows_plugin.h> #include <permission_handler_windows/permission_handler_windows_plugin.h>
#include <screen_retriever_windows/screen_retriever_windows_plugin_c_api.h>
#include <share_plus/share_plus_windows_plugin_c_api.h> #include <share_plus/share_plus_windows_plugin_c_api.h>
#include <url_launcher_windows/url_launcher_windows.h> #include <url_launcher_windows/url_launcher_windows.h>
#include <window_manager/window_manager_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) { void RegisterPlugins(flutter::PluginRegistry* registry) {
DynamicColorPluginCApiRegisterWithRegistrar( DynamicColorPluginCApiRegisterWithRegistrar(
@@ -22,8 +24,12 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("FlutterInappwebviewWindowsPluginCApi")); registry->GetRegistrarForPlugin("FlutterInappwebviewWindowsPluginCApi"));
PermissionHandlerWindowsPluginRegisterWithRegistrar( PermissionHandlerWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi"));
SharePlusWindowsPluginCApiRegisterWithRegistrar( SharePlusWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
UrlLauncherWindowsRegisterWithRegistrar( UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows")); registry->GetRegistrarForPlugin("UrlLauncherWindows"));
WindowManagerPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("WindowManagerPlugin"));
} }

View File

@@ -7,8 +7,10 @@ list(APPEND FLUTTER_PLUGIN_LIST
file_selector_windows file_selector_windows
flutter_inappwebview_windows flutter_inappwebview_windows
permission_handler_windows permission_handler_windows
screen_retriever_windows
share_plus share_plus
url_launcher_windows url_launcher_windows
window_manager
) )
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST