From 5dc8d648579344df732629c98d5ee815cd301fcd Mon Sep 17 00:00:00 2001 From: DelLevin-Home Date: Wed, 15 Jul 2026 02:57:41 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96windows=E7=AC=94=E8=AE=B0?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/vditor/dist/vditor_editor.html | 12 + lib/data/note/note_dao.dart | 5 +- lib/main.dart | 14 + lib/pages/book/book_detail_page.dart | 9 +- lib/pages/epub_reader/epub_detail_page.dart | 280 +++++++-------- lib/pages/epub_reader/epub_library_page.dart | 107 +++--- lib/pages/home/home_page.dart | 177 +++++++++- lib/pages/note/note_add_page.dart | 164 +++++---- lib/pages/note/note_detail_page.dart | 322 +++++++++++------- lib/utils/theme/app_theme.dart | 33 ++ lib/utils/toast_util.dart | 4 +- lib/widgets/app_shell.dart | 19 ++ lib/widgets/custom_title_bar.dart | 148 ++++++++ lib/widgets/vditor_editor.dart | 24 ++ linux/flutter/generated_plugin_registrant.cc | 8 + linux/flutter/generated_plugins.cmake | 2 + macos/Flutter/GeneratedPluginRegistrant.swift | 4 + pubspec.lock | 48 +++ pubspec.yaml | 1 + .../flutter/generated_plugin_registrant.cc | 6 + windows/flutter/generated_plugins.cmake | 2 + 21 files changed, 983 insertions(+), 406 deletions(-) create mode 100644 lib/widgets/app_shell.dart create mode 100644 lib/widgets/custom_title_bar.dart 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