diff --git a/assets/icon/app_icon.png b/assets/icon/app_icon.png deleted file mode 100644 index 72fc5bc..0000000 Binary files a/assets/icon/app_icon.png and /dev/null differ diff --git a/assets/icon/app_icon.webp b/assets/icon/app_icon.webp new file mode 100644 index 0000000..4125d46 Binary files /dev/null and b/assets/icon/app_icon.webp differ diff --git a/assets/icon/app_icon2.png b/assets/icon/app_icon2.png deleted file mode 100644 index e23ba03..0000000 Binary files a/assets/icon/app_icon2.png and /dev/null differ diff --git a/assets/icon/app_icon2.webp b/assets/icon/app_icon2.webp new file mode 100644 index 0000000..4465b72 Binary files /dev/null and b/assets/icon/app_icon2.webp differ diff --git a/lib/main.dart b/lib/main.dart index 8f032c3..bc3f243 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,7 +6,6 @@ import 'package:flutter/services.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:provider/provider.dart'; import 'package:dynamic_color/dynamic_color.dart'; -import 'package:flutter_quill/flutter_quill.dart' as quill; import 'package:url_launcher/url_launcher.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; @@ -18,7 +17,6 @@ import 'utils/changelog_service.dart'; import 'utils/sync/auto_backup_service.dart'; import 'utils/usage_stats_service.dart'; import 'providers/app_provider.dart'; -import 'providers/note_plus_provider.dart'; final RouteObserver> routeObserver = RouteObserver>(); @@ -252,7 +250,6 @@ class _MyAppState extends State with WidgetsBindingObserver { return MultiProvider( providers: [ ChangeNotifierProvider.value(value: widget.appProvider), - ChangeNotifierProvider(create: (_) => NotePlusProvider()), ], child: DynamicColorBuilder( builder: (lightScheme, darkScheme) { @@ -305,7 +302,6 @@ class _MyAppState extends State with WidgetsBindingObserver { GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate, - quill.FlutterQuillLocalizations.delegate, ], supportedLocales: const [ Locale('zh', 'CN'), diff --git a/lib/models/note_plus_models.dart b/lib/models/note_plus_models.dart deleted file mode 100644 index c3d74ce..0000000 --- a/lib/models/note_plus_models.dart +++ /dev/null @@ -1,318 +0,0 @@ -import 'dart:convert'; -import 'package:flutter/foundation.dart'; -import 'package:uuid/uuid.dart'; - -/// 块类型枚举 -enum NoteBlockType { - paragraph, - heading1, - heading2, - heading3, - bulletList, - numberedList, - checklist, - quote, - codeBlock, - divider; - - String toJson() => name; - - static NoteBlockType fromString(String value) { - return NoteBlockType.values.firstWhere( - (t) => t.name == value, - orElse: () => NoteBlockType.paragraph, - ); - } - - /// 中文标签 - String get label { - switch (this) { - case NoteBlockType.paragraph: - return '正文'; - case NoteBlockType.heading1: - return '标题1'; - case NoteBlockType.heading2: - return '标题2'; - case NoteBlockType.heading3: - return '标题3'; - case NoteBlockType.bulletList: - return '无序列表'; - case NoteBlockType.numberedList: - return '有序列表'; - case NoteBlockType.checklist: - return '待办'; - case NoteBlockType.quote: - return '引用'; - case NoteBlockType.codeBlock: - return '代码块'; - case NoteBlockType.divider: - return '分割线'; - } - } -} - -/// 内联格式类型 -enum InlineFormatType { - bold, - italic, - underline, - strikethrough, - inlineCode; - - String toJson() => name; - - static InlineFormatType fromString(String value) { - return InlineFormatType.values.firstWhere( - (t) => t.name == value, - orElse: () => InlineFormatType.bold, - ); - } -} - -/// 内联格式区间 -class InlineFormatSpan { - final int start; - final int end; - final Set formats; - - const InlineFormatSpan({ - required this.start, - required this.end, - required this.formats, - }); - - Map toJson() => { - 's': start, - 'e': end, - 'f': formats.map((f) => f.toJson()).toList(), - }; - - factory InlineFormatSpan.fromJson(Map json) { - return InlineFormatSpan( - start: json['s'] as int? ?? 0, - end: json['e'] as int? ?? 0, - formats: (json['f'] as List?) - ?.map((f) => InlineFormatType.fromString(f as String)) - .toSet() ?? - {}, - ); - } - - InlineFormatSpan copyWith( - {int? start, int? end, Set? formats}) { - return InlineFormatSpan( - start: start ?? this.start, - end: end ?? this.end, - formats: formats ?? Set.from(this.formats), - ); - } -} - -/// 单个内容块 -class NoteBlock { - final String id; - final NoteBlockType type; - final String text; - final Map metadata; - final List formatting; - - NoteBlock({ - String? id, - this.type = NoteBlockType.paragraph, - this.text = '', - Map? metadata, - List? formatting, - }) : id = id ?? const Uuid().v4(), - metadata = metadata ?? {}, - formatting = formatting ?? []; - - Map toJson() => { - 'id': id, - 'type': type.toJson(), - 'text': text, - 'metadata': metadata, - 'formatting': formatting.map((f) => f.toJson()).toList(), - }; - - factory NoteBlock.fromJson(Map json) { - return NoteBlock( - id: json['id'] as String?, - type: NoteBlockType.fromString(json['type'] as String? ?? 'paragraph'), - text: json['text'] as String? ?? '', - metadata: (json['metadata'] as Map?) ?? {}, - formatting: (json['formatting'] as List?) - ?.map( - (f) => InlineFormatSpan.fromJson(f as Map)) - .toList() ?? - [], - ); - } - - NoteBlock copyWith({ - NoteBlockType? type, - String? text, - Map? metadata, - List? formatting, - }) { - return NoteBlock( - id: id, - type: type ?? this.type, - text: text ?? this.text, - metadata: metadata ?? Map.from(this.metadata), - formatting: formatting ?? List.from(this.formatting), - ); - } - - /// 深拷贝 - NoteBlock deepCopy() { - return NoteBlock( - id: id, - type: type, - text: text, - metadata: Map.from(metadata), - formatting: formatting - .map((f) => InlineFormatSpan( - start: f.start, - end: f.end, - formats: Set.from(f.formats), - )) - .toList(), - ); - } -} - -/// Note Plus 文档 -/// -/// 仿 AppFlowy 的 View 模型: -/// - parentId 指向父文档 ID(空字符串 = 根级文档) -/// - 每个文档既是页面也是容器,支持无限嵌套 -class NotePlusDocument { - final String id; - final String title; - final String parentId; // 父文档 ID,空字符串 = 根级 - final int sortIndex; // 同级排序序号 - final List blocks; - final String? blocksJson; // 原始 Delta JSON(flutter_quill 模式优先使用) - final List tags; - final List images; - final DateTime createdAt; - final DateTime updatedAt; - final bool isDeleted; - - NotePlusDocument({ - String? id, - this.title = '', - this.parentId = '', - this.sortIndex = 0, - List? blocks, - this.blocksJson, - List? tags, - List? images, - DateTime? createdAt, - DateTime? updatedAt, - this.isDeleted = false, - }) : id = id ?? const Uuid().v4(), - blocks = blocks ?? [NoteBlock()], - tags = tags ?? [], - images = images ?? [], - createdAt = createdAt ?? DateTime.now(), - updatedAt = updatedAt ?? DateTime.now(); - - /// 从 JSON(DB 行)创建 - factory NotePlusDocument.fromJson(Map json) { - final blocksJson = json['blocks_json'] as String? ?? '[]'; - List blocksList; - try { - blocksList = (jsonDecode(blocksJson) as List) - .map((b) => NoteBlock.fromJson(b as Map)) - .toList(); - } catch (e) { - debugPrint('[NotePlusDocument] blocks_json 解析失败,使用默认空文档: $e'); - blocksList = []; - } - - return NotePlusDocument( - id: json['id'] as String?, - title: json['title'] as String? ?? '', - parentId: json['parent_id'] as String? ?? '', - sortIndex: json['sort_index'] as int? ?? 0, - blocks: blocksList.isEmpty ? [NoteBlock()] : blocksList, - blocksJson: json['blocks_json'] as String?, - tags: _parseStringList(json['tags'] as String?), - images: _parseStringList(json['images'] as String?), - createdAt: json['created_at'] != null - ? (DateTime.tryParse(json['created_at'] as String)?.toLocal() ?? DateTime.now()) - : DateTime.now(), - updatedAt: json['updated_at'] != null - ? (DateTime.tryParse(json['updated_at'] as String)?.toLocal() ?? DateTime.now()) - : DateTime.now(), - isDeleted: (json['is_deleted'] as int?) == 1, - ); - } - - /// 序列化为 DB 行 - Map toJson() => { - 'id': id, - 'title': title, - 'parent_id': parentId, - 'sort_index': sortIndex, - 'blocks_json': blocksJson ?? jsonEncode(blocks.map((b) => b.toJson()).toList()), - 'tags': tags.isEmpty ? null : jsonEncode(tags), - 'images': images.isEmpty ? null : jsonEncode(images), - 'created_at': createdAt.toUtc().toIso8601String(), - 'updated_at': updatedAt.toUtc().toIso8601String(), - 'is_deleted': isDeleted ? 1 : 0, - }; - - NotePlusDocument copyWith({ - String? title, - String? parentId, - int? sortIndex, - List? blocks, - String? blocksJson, - List? tags, - List? images, - DateTime? updatedAt, - bool? isDeleted, - }) { - return NotePlusDocument( - id: id, - title: title ?? this.title, - parentId: parentId ?? this.parentId, - sortIndex: sortIndex ?? this.sortIndex, - blocks: blocks ?? List.from(this.blocks), - blocksJson: blocksJson ?? this.blocksJson, - tags: tags ?? List.from(this.tags), - images: images ?? List.from(this.images), - createdAt: createdAt, - updatedAt: updatedAt ?? this.updatedAt, - isDeleted: isDeleted ?? this.isDeleted, - ); - } - - /// 深拷贝 - NotePlusDocument deepCopy() { - return NotePlusDocument( - id: id, - title: title, - parentId: parentId, - sortIndex: sortIndex, - blocks: blocks.map((b) => b.deepCopy()).toList(), - tags: List.from(tags), - images: List.from(images), - createdAt: createdAt, - updatedAt: updatedAt, - isDeleted: isDeleted, - ); - } - - static List _parseStringList(String? json) { - if (json == null || json.isEmpty) return []; - try { - final list = jsonDecode(json) as List; - return list.map((e) => e.toString()).toList(); - } catch (_) { - return []; - } - } -} diff --git a/lib/pages/app_icon_picker_page.dart b/lib/pages/app_icon_picker_page.dart index cbb9960..24bd941 100644 --- a/lib/pages/app_icon_picker_page.dart +++ b/lib/pages/app_icon_picker_page.dart @@ -88,7 +88,7 @@ class _AppIconPickerPageState extends State { ), clipBehavior: Clip.antiAlias, child: Image.asset( - 'assets/icon/${icon['name']}.png', + 'assets/icon/${icon['name']}${icon['name'] == 'app_icon_m' ? '.png' : '.webp'}', width: 40, height: 40, fit: BoxFit.cover, diff --git a/lib/pages/main_content_page.dart b/lib/pages/main_content_page.dart index 80cca2a..784dac8 100644 --- a/lib/pages/main_content_page.dart +++ b/lib/pages/main_content_page.dart @@ -6,7 +6,6 @@ import '../utils/sync/webdav_service.dart'; import 'movies/movie_tab_page.dart'; import 'book/book_tab_page.dart'; import 'note/note_tab_page.dart'; -import 'note_plus/note_plus_tab_page.dart'; import 'online_search/search_page.dart'; import 'online_search/online_search_page.dart'; import 'sync/webdav_sync_page.dart'; @@ -25,7 +24,6 @@ class _MainContentPageState extends State { bool _showMovieTab = true; bool _showBookTab = true; bool _showNoteTab = true; - bool _showNotePlusTab = false; late PageController _pageController; bool _isTabTap = false; @@ -49,7 +47,6 @@ class _MainContentPageState extends State { _showMovieTab = _userPrefs.showMovieTab; _showBookTab = _userPrefs.showBookTab; _showNoteTab = _userPrefs.showNoteTab; - _showNotePlusTab = _userPrefs.showNotePlusTab; }); } @@ -64,7 +61,6 @@ class _MainContentPageState extends State { if (_showMovieTab) tabs.add(_TabItem('影视', 0)); if (_showBookTab) tabs.add(_TabItem('阅读', 1)); if (_showNoteTab) tabs.add(_TabItem('笔记', 2)); - if (_showNotePlusTab) tabs.add(_TabItem('Plus', 3)); return tabs; } @@ -138,7 +134,6 @@ class _MainContentPageState extends State { case 0: return '影视'; case 1: return '阅读'; case 2: return '笔记'; - case 3: return 'Note Plus'; default: return 'MookNote'; } } @@ -439,7 +434,6 @@ class _MainContentPageState extends State { if (_showMovieTab) const MovieTabPage(), if (_showBookTab) const BookTabPage(), if (_showNoteTab) const NoteTabPage(), - if (_showNotePlusTab) const NotePlusTabPage(), ], ); }, @@ -451,7 +445,6 @@ class _MainContentPageState extends State { case '影视': return Icons.movie_outlined; case '阅读': return Icons.menu_book_outlined; case '笔记': return Icons.note_outlined; - case 'Plus': return Icons.edit_note; default: return Icons.circle; } } diff --git a/lib/pages/movies/douban_webview_page.dart b/lib/pages/movies/douban_webview_page.dart index 870ed06..ea9c765 100644 --- a/lib/pages/movies/douban_webview_page.dart +++ b/lib/pages/movies/douban_webview_page.dart @@ -1,6 +1,6 @@ import 'dart:convert'; import 'package:flutter/material.dart'; -import 'package:webview_flutter/webview_flutter.dart'; +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; /// 豆瓣影视WebView页面 - 用于抓取影视信息 class DoubanWebViewPage extends StatefulWidget { @@ -13,47 +13,10 @@ class DoubanWebViewPage extends StatefulWidget { } class _DoubanWebViewPageState extends State { - late WebViewController _controller; + InAppWebViewController? _controller; bool _isLoading = true; bool _isExtracting = false; // 防止重复提取 - @override - void initState() { - super.initState(); - _initWebView(); - } - - @override - void dispose() { - // 清理 WebView 资源 - _controller.loadRequest(Uri.parse('about:blank')); - super.dispose(); - } - - void _initWebView() { - _controller = WebViewController() - ..setJavaScriptMode(JavaScriptMode.unrestricted) - ..setNavigationDelegate( - NavigationDelegate( - onPageStarted: (String url) { - if (mounted) { - setState(() { - _isLoading = true; - }); - } - }, - onPageFinished: (String url) { - if (mounted) { - setState(() { - _isLoading = false; - }); - } - }, - ), - ) - ..loadRequest(Uri.parse(widget.url)); - } - @override Widget build(BuildContext context) { final colors = Theme.of(context).colorScheme; @@ -74,7 +37,7 @@ class _DoubanWebViewPageState extends State { _buildActionButton( colors: colors, icon: Icons.refresh, - onPressed: () => _controller.reload(), + onPressed: () => _controller?.reload(), tooltip: '刷新', ), const SizedBox(width: 8), @@ -82,7 +45,29 @@ class _DoubanWebViewPageState extends State { ), body: Stack( children: [ - WebViewWidget(controller: _controller), + InAppWebView( + initialUrlRequest: URLRequest(url: WebUri(widget.url)), + initialSettings: InAppWebViewSettings( + javaScriptEnabled: true, + ), + onWebViewCreated: (controller) { + _controller = controller; + }, + onLoadStart: (_, __) { + if (mounted) { + setState(() { + _isLoading = true; + }); + } + }, + onLoadStop: (_, __) { + if (mounted) { + setState(() { + _isLoading = false; + }); + } + }, + ), // 加载指示器 if (_isLoading) const Center( child: CircularProgressIndicator(), @@ -105,7 +90,8 @@ class _DoubanWebViewPageState extends State { child: InkWell( onTap: () { // 停止加载并返回 - _controller.loadRequest(Uri.parse('about:blank')); + _controller?.loadUrl( + urlRequest: URLRequest(url: WebUri('about:blank'))); Navigator.pop(context); }, borderRadius: BorderRadius.circular(8), @@ -250,7 +236,7 @@ class _DoubanWebViewPageState extends State { /// 提取影视信息 Future?> _extractMovieInfo() async { // 检查是否已提取过,避免重复点击 - if (_isExtracting) return null; + if (_isExtracting || _controller == null) return null; try { _isExtracting = true; @@ -265,7 +251,7 @@ class _DoubanWebViewPageState extends State { ); // 执行JavaScript代码提取页面信息 - final result = await _controller.runJavaScriptReturningResult(r''' + final result = await _controller!.evaluateJavascript(source: r''' (function() { const info = {}; @@ -384,9 +370,11 @@ class _DoubanWebViewPageState extends State { if (mounted) Navigator.pop(context); // 解析提取的信息 - // result 是 JavaScript 执行结果,已经是 JSON 字符串(带引号的) - final String jsonStr = result.toString(); - // 去除 Dart 字符串转义后外层可能多余的引号 + // evaluateJavascript 返回 JS 值,JSON.stringify 的结果是字符串 + final String jsonStr = result?.toString() ?? ''; + if (jsonStr.isEmpty) return null; + + // result 是 JSON.stringify 的输出,可能带外层引号 final String cleanJson = jsonStr.startsWith('"') && jsonStr.endsWith('"') ? jsonDecode(jsonStr) as String : jsonStr; diff --git a/lib/pages/note_plus/note_plus_detail_page.dart b/lib/pages/note_plus/note_plus_detail_page.dart deleted file mode 100644 index 69adc15..0000000 --- a/lib/pages/note_plus/note_plus_detail_page.dart +++ /dev/null @@ -1,278 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import '../../models/note_plus_models.dart'; -import '../../providers/note_plus_provider.dart'; - -/// Note Plus 只读查看页 -class NotePlusDetailPage extends StatelessWidget { - final String documentId; - - const NotePlusDetailPage({super.key, required this.documentId}); - - @override - Widget build(BuildContext context) { - final colors = Theme.of(context).colorScheme; - - return FutureBuilder( - future: context.read().getDeletedDocuments().then( - (_) => context.read().currentDocument?.id == documentId - ? context.read().currentDocument - : null), - builder: (context, snapshot) { - // 直接从 provider 取 - return Consumer( - builder: (context, provider, _) { - final doc = provider.currentDocument; - if (doc == null || doc.id != documentId) { - // 加载文档(延迟到 build 完成后执行,避免副作用) - WidgetsBinding.instance.addPostFrameCallback((_) { - provider.loadDocumentById(documentId); - }); - return Scaffold( - backgroundColor: colors.surface, - body: const Center(child: CircularProgressIndicator()), - ); - } - - return Scaffold( - backgroundColor: colors.surface, - appBar: AppBar( - title: Text(doc.title.isEmpty ? '无标题' : doc.title), - backgroundColor: colors.surface, - surfaceTintColor: Colors.transparent, - actions: [ - IconButton( - icon: const Icon(Icons.edit), - onPressed: () { - Navigator.pushNamed(context, '/note-plus-form', - arguments: doc.id); - }, - ), - ], - ), - body: ListView.builder( - padding: const EdgeInsets.fromLTRB(16, 16, 16, 80), - itemCount: doc.blocks.length, - itemBuilder: (context, index) { - return _buildBlock(doc.blocks[index], index, doc.blocks, colors); - }, - ), - ); - }, - ); - }, - ); - } - - Widget _buildBlock(NoteBlock block, int index, List allBlocks, - ColorScheme colors) { - if (block.type == NoteBlockType.divider) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 12), - child: Container(height: 1, color: colors.outlineVariant), - ); - } - - final style = _getTextStyle(block, colors); - final text = block.text.isEmpty ? '(空)' : block.text; - - return Padding( - padding: _getBlockPadding(block), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildPrefix(block, index, allBlocks, colors), - Expanded( - child: block.type == NoteBlockType.codeBlock - ? Container( - decoration: BoxDecoration( - color: Colors.grey.shade50, - borderRadius: BorderRadius.circular(2), - ), - padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12), - child: block.text.isEmpty - ? Text('(空)', style: style.copyWith( - color: colors.onSurface.withValues(alpha: 0.2))) - : _buildRichText(text, style, block, colors), - ) - : block.text.isEmpty - ? Text(text, style: style.copyWith( - color: colors.onSurface.withValues(alpha: 0.2))) - : _buildRichText(text, style, block, colors), - ), - ], - ), - ); - } - - Widget _buildPrefix(NoteBlock block, int index, List allBlocks, - ColorScheme colors) { - switch (block.type) { - case NoteBlockType.bulletList: - return Padding( - padding: const EdgeInsets.only(top: 8, right: 8), - child: Container( - width: 6, height: 6, - decoration: BoxDecoration( - color: colors.onSurface.withValues(alpha: 0.5), - shape: BoxShape.circle, - ), - ), - ); - case NoteBlockType.numberedList: - int num = 1; - for (int i = index - 1; i >= 0; i--) { - if (allBlocks[i].type == NoteBlockType.numberedList) { - num++; - } else { - break; - } - } - return Padding( - padding: const EdgeInsets.only(top: 6, right: 8), - child: Text('$num.', style: TextStyle( - fontSize: 14, color: colors.onSurface.withValues(alpha: 0.5))), - ); - case NoteBlockType.checklist: - return Padding( - padding: const EdgeInsets.only(top: 4, right: 8), - child: Icon( - block.metadata['checked'] == true - ? Icons.check_box - : Icons.check_box_outline_blank, - size: 20, - color: block.metadata['checked'] == true - ? colors.primary - : colors.onSurface.withValues(alpha: 0.4), - ), - ); - case NoteBlockType.quote: - return Container( - width: 4, - margin: const EdgeInsets.only(top: 6, bottom: 6, right: 12), - decoration: BoxDecoration( - color: Colors.grey.shade300, - borderRadius: BorderRadius.circular(2), - ), - ); - default: - return const SizedBox.shrink(); - } - } - - Widget _buildRichText(String text, TextStyle style, NoteBlock block, - ColorScheme colors) { - if (block.formatting.isEmpty) return Text(text, style: style); - - final spans = []; - final events = <_FmtEvent>[]; - for (final span in block.formatting) { - if (span.start < text.length) { - events.add(_FmtEvent(span.start, true, span.formats)); - events.add(_FmtEvent( - span.end > text.length ? text.length : span.end, false, span.formats)); - } - } - events.sort((a, b) => a.pos.compareTo(b.pos)); - - int lastPos = 0; - final active = {}; - for (final e in events) { - if (e.pos > lastPos) { - spans.add(TextSpan( - text: text.substring(lastPos, e.pos), - style: _applyFormats(style, active, colors), - )); - } - if (e.isStart) { - active.addAll(e.formats); - } else { - active.removeAll(e.formats); - } - lastPos = e.pos; - } - if (lastPos < text.length) { - spans.add(TextSpan( - text: text.substring(lastPos), - style: _applyFormats(style, active, colors), - )); - } - - return Text.rich(TextSpan(children: spans)); - } - - TextStyle _applyFormats(TextStyle style, Set formats, - ColorScheme colors) { - if (formats.isEmpty) return style; - return style.copyWith( - fontWeight: formats.contains(InlineFormatType.bold) ? FontWeight.w700 : null, - fontStyle: formats.contains(InlineFormatType.italic) ? FontStyle.italic : null, - decoration: _getDecoration(formats), - fontFamily: formats.contains(InlineFormatType.inlineCode) ? 'monospace' : null, - backgroundColor: formats.contains(InlineFormatType.inlineCode) - ? colors.surfaceContainerHighest : null, - ); - } - - TextDecoration? _getDecoration(Set formats) { - final list = []; - if (formats.contains(InlineFormatType.underline)) list.add(TextDecoration.underline); - if (formats.contains(InlineFormatType.strikethrough)) list.add(TextDecoration.lineThrough); - return list.isEmpty ? null : TextDecoration.combine(list); - } - - TextStyle _getTextStyle(NoteBlock block, ColorScheme colors) { - // AppFlowy base: 18px, w300, height 1.3, letter-spacing 0.6 - final base = TextStyle( - color: colors.onSurface, - fontSize: 18, fontWeight: FontWeight.w300, height: 1.3, letterSpacing: 0.6, - ); - switch (block.type) { - case NoteBlockType.heading1: - return base.copyWith(fontSize: 34, fontWeight: FontWeight.w300, height: 1.15, - letterSpacing: 0, color: colors.onSurface.withValues(alpha: 0.7)); - case NoteBlockType.heading2: - return base.copyWith(fontSize: 24, fontWeight: FontWeight.w400, height: 1.15, - letterSpacing: 0, color: colors.onSurface.withValues(alpha: 0.7)); - case NoteBlockType.heading3: - return base.copyWith(fontSize: 20, fontWeight: FontWeight.w500, height: 1.25, - letterSpacing: 0, color: colors.onSurface.withValues(alpha: 0.7)); - case NoteBlockType.quote: - return base.copyWith(color: colors.onSurface.withValues(alpha: 0.6)); - case NoteBlockType.codeBlock: - return base.copyWith(fontSize: 13, fontWeight: FontWeight.w400, fontFamily: 'monospace', - height: 1.15, letterSpacing: 0, color: Colors.blue.shade900.withValues(alpha: 0.9)); - case NoteBlockType.checklist: - return base.copyWith( - decoration: block.metadata['checked'] == true ? TextDecoration.lineThrough : null, - color: block.metadata['checked'] == true ? colors.onSurface.withValues(alpha: 0.4) : null, - ); - default: - return base; - } - } - - EdgeInsets _getBlockPadding(NoteBlock block) { - switch (block.type) { - case NoteBlockType.heading1: - return const EdgeInsets.only(top: 16); - case NoteBlockType.heading2: - return const EdgeInsets.only(top: 8); - case NoteBlockType.heading3: - return const EdgeInsets.only(top: 8); - case NoteBlockType.codeBlock: - return const EdgeInsets.symmetric(vertical: 6); - case NoteBlockType.quote: - return const EdgeInsets.only(top: 6, bottom: 2); - default: - return const EdgeInsets.only(top: 10); - } - } -} - -class _FmtEvent { - final int pos; - final bool isStart; - final Set formats; - _FmtEvent(this.pos, this.isStart, this.formats); -} diff --git a/lib/pages/note_plus/note_plus_form_page.dart b/lib/pages/note_plus/note_plus_form_page.dart deleted file mode 100644 index dcfdd5d..0000000 --- a/lib/pages/note_plus/note_plus_form_page.dart +++ /dev/null @@ -1,448 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'package:flutter/material.dart'; -import 'package:flutter_quill/flutter_quill.dart' as quill; -import 'package:provider/provider.dart'; -import '../../providers/note_plus_provider.dart'; -import '../../utils/toast_util.dart'; - -/// Note Plus 编辑页 -class NotePlusFormPage extends StatefulWidget { - final String documentId; - - const NotePlusFormPage({super.key, required this.documentId}); - - @override - State createState() => _NotePlusFormPageState(); -} - -class _NotePlusFormPageState extends State { - final _titleController = TextEditingController(); - final _titleFocus = FocusNode(); - final _scrollController = ScrollController(); - final _editorFocus = FocusNode(); - quill.QuillController? _controller; - bool _isInitialized = false; - Timer? _autoSaveTimer; - int _charCount = 0; - - @override - void dispose() { - _autoSaveTimer?.cancel(); - _titleController.dispose(); - _titleFocus.dispose(); - _scrollController.dispose(); - _editorFocus.dispose(); - _controller?.dispose(); - super.dispose(); - } - - // ─── 初始化 ───────────────────────────────────── - - void _initDocument(NotePlusProvider provider) { - if (_isInitialized) return; - _isInitialized = true; - - provider.loadDocumentById(widget.documentId).then((_) { - if (!mounted || provider.currentDocument == null) return; - - _titleController.text = provider.currentDocument!.title; - - quill.Document doc; - final raw = provider.currentDocument!.blocksJson; - if (raw != null && raw.isNotEmpty && raw.startsWith('[')) { - try { - final parsed = jsonDecode(raw) as List; - if (parsed.isNotEmpty && - parsed.first is Map && - (parsed.first as Map).containsKey('insert')) { - doc = quill.Document.fromJson(parsed); - } else { - doc = quill.Document(); - } - } catch (_) { - doc = quill.Document(); - } - } else { - doc = quill.Document(); - } - - setState(() { - _controller = quill.QuillController( - document: doc, - selection: const TextSelection.collapsed(offset: 0), - ); - _controller!.addListener(_onDocChanged); - _charCount = _controller!.document.length - 1; // 去掉末尾 \n - }); - }); - } - - // ─── 文档变化监听 ───────────────────────────────── - - void _onDocChanged() { - if (_controller == null) return; - final newCount = _controller!.document.length - 1; - if (newCount != _charCount) { - setState(() => _charCount = newCount); - } - // 自动保存(防抖 2 秒) - _autoSaveTimer?.cancel(); - _autoSaveTimer = Timer(const Duration(seconds: 2), () { - if (mounted) _autoSave(); - }); - } - - void _autoSave() { - final provider = context.read(); - if (_controller == null) return; - provider.setTitle(_titleController.text); - final deltaJson = jsonEncode(_controller!.document.toDelta().toJson()); - provider.saveDocument(deltaJson: deltaJson); - } - - void _save(NotePlusProvider provider) async { - _autoSaveTimer?.cancel(); - if (_controller == null) return; - provider.setTitle(_titleController.text); - final deltaJson = jsonEncode(_controller!.document.toDelta().toJson()); - await provider.saveDocument(deltaJson: deltaJson); - if (mounted) ToastUtil.show(context, '已保存'); - } - - // ─── 构建 ───────────────────────────────────── - - @override - Widget build(BuildContext context) { - final colors = Theme.of(context).colorScheme; - - return Consumer( - builder: (context, provider, _) { - _initDocument(provider); - - return PopScope( - canPop: !provider.isDirty, - onPopInvokedWithResult: (didPop, _) { - if (!didPop && provider.isDirty) { - _showSaveDialog(provider); - } else { - _autoSave(); // 退出时自动保存 - } - }, - child: Scaffold( - backgroundColor: colors.surface, - appBar: _buildAppBar(provider, colors), - body: _controller == null - ? const Center(child: CircularProgressIndicator()) - : GestureDetector( - // 点击空白区域聚焦编辑器 - onTap: () => _editorFocus.requestFocus(), - behavior: HitTestBehavior.translucent, - child: Column( - children: [ - // 标题区 - _buildTitleArea(colors), - // 分隔线 - Divider(height: 1, color: colors.outlineVariant.withValues(alpha: 0.3)), - // 编辑器 - Expanded(child: _buildEditor(colors)), - // 工具栏 - _buildToolbar(colors), - ], - ), - ), - ), - ); - }, - ); - } - - // ─── AppBar ───────────────────────────────────── - - PreferredSizeWidget _buildAppBar(NotePlusProvider provider, ColorScheme colors) { - return AppBar( - backgroundColor: colors.surface, - surfaceTintColor: Colors.transparent, - elevation: 0, - leading: IconButton( - icon: const Icon(Icons.arrow_back, size: 22), - onPressed: () { - _autoSave(); - Navigator.pop(context); - }, - ), - title: Text( - '$_charCount 字', - style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.35)), - ), - centerTitle: true, - actions: [ - // 未保存指示 - if (provider.isDirty) - Padding( - padding: const EdgeInsets.only(right: 4), - child: Center( - child: Container( - width: 6, height: 6, - decoration: BoxDecoration(color: colors.error, shape: BoxShape.circle), - ), - ), - ), - IconButton( - icon: const Icon(Icons.undo, size: 20), - onPressed: _controller != null ? () => _controller!.undo() : null, - tooltip: '撤销', - ), - IconButton( - icon: const Icon(Icons.redo, size: 20), - onPressed: _controller != null ? () => _controller!.redo() : null, - tooltip: '重做', - ), - IconButton( - icon: Icon(Icons.check, size: 22, color: colors.primary), - onPressed: () => _save(provider), - tooltip: '保存', - ), - ], - ); - } - - // ─── 标题区 ───────────────────────────────────── - - Widget _buildTitleArea(ColorScheme colors) { - return Padding( - padding: const EdgeInsets.fromLTRB(20, 16, 20, 8), - child: TextField( - controller: _titleController, - focusNode: _titleFocus, - style: TextStyle( - fontSize: 28, - fontWeight: FontWeight.w600, - color: colors.onSurface, - height: 1.3, - ), - decoration: InputDecoration( - hintText: '输入标题...', - hintStyle: TextStyle( - fontSize: 28, - fontWeight: FontWeight.w600, - color: colors.onSurface.withValues(alpha: 0.15), - ), - border: InputBorder.none, - contentPadding: EdgeInsets.zero, - isDense: true, - ), - onChanged: (_) => context.read().setTitle(_titleController.text), - textInputAction: TextInputAction.next, - onSubmitted: (_) => _editorFocus.requestFocus(), - ), - ); - } - - // ─── 编辑器 ───────────────────────────────────── - - Widget _buildEditor(ColorScheme colors) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), - child: TextSelectionTheme( - data: TextSelectionThemeData( - selectionColor: colors.primary.withValues(alpha: 0.12), - selectionHandleColor: colors.primary, - cursorColor: colors.primary, - ), - child: quill.QuillEditor.basic( - controller: _controller!, - focusNode: _editorFocus, - scrollController: _scrollController, - config: const quill.QuillEditorConfig( - padding: EdgeInsets.symmetric(horizontal: 8, vertical: 8), - autoFocus: false, - expands: true, - ), - ), - ), - ); - } - - // ─── 工具栏 ───────────────────────────────────── - - Widget _buildToolbar(ColorScheme colors) { - return Container( - decoration: BoxDecoration( - color: colors.surface, - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.04), - blurRadius: 8, - offset: const Offset(0, -2), - ), - ], - ), - padding: const EdgeInsets.fromLTRB(12, 6, 12, 6), - child: SafeArea( - top: false, - child: Container( - decoration: BoxDecoration( - color: colors.surfaceContainerHighest, - borderRadius: BorderRadius.circular(14), - ), - padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 3), - child: quill.QuillSimpleToolbar( - controller: _controller!, - config: quill.QuillSimpleToolbarConfig( - multiRowsDisplay: false, - color: Colors.transparent, - toolbarSize: 30, - buttonOptions: const quill.QuillSimpleToolbarButtonOptions( - base: quill.QuillToolbarBaseButtonOptions(iconSize: 17), - ), - showBoldButton: true, - showItalicButton: true, - showUnderLineButton: true, - showStrikeThrough: true, - showHeaderStyle: false, - showListBullets: true, - showListNumbers: true, - showListCheck: true, - showCodeBlock: true, - showQuote: true, - showInlineCode: true, - showUndo: false, - showRedo: false, - showLink: false, - showSearchButton: false, - showFontSize: false, - showFontFamily: false, - showColorButton: false, - showBackgroundColorButton: false, - showClearFormat: false, - showAlignmentButtons: false, - showDirection: false, - showIndent: false, - showSubscript: false, - showSuperscript: false, - customButtons: [ - quill.QuillToolbarCustomButtonOptions( - icon: const Icon(Icons.text_fields, size: 17, color: Color(0xFF555555)), - onPressed: () => _showHeaderPicker(colors), - ), - ], - ), - ), - ), - ), - ); - } - - // ─── 标题选择 ───────────────────────────────── - - void _showHeaderPicker(ColorScheme colors) { - final current = _controller?.getSelectionStyle().attributes ?? {}; - final currentHeader = current['header']?.value; - - showModalBottomSheet( - context: context, - backgroundColor: Colors.transparent, - builder: (ctx) => Container( - margin: const EdgeInsets.fromLTRB(16, 0, 16, 16), - decoration: BoxDecoration( - color: colors.surface, - borderRadius: BorderRadius.circular(14), - boxShadow: [ - BoxShadow(color: Colors.black.withValues(alpha: 0.1), blurRadius: 20), - ], - ), - child: SafeArea( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // 拖拽指示条 - Container( - width: 36, height: 4, - margin: const EdgeInsets.only(top: 10, bottom: 8), - decoration: BoxDecoration( - color: colors.onSurface.withValues(alpha: 0.12), - borderRadius: BorderRadius.circular(2), - ), - ), - _headerOption('正文', null, currentHeader == null, colors), - _headerOption('标题 1', 1, currentHeader == 1, colors), - _headerOption('标题 2', 2, currentHeader == 2, colors), - _headerOption('标题 3', 3, currentHeader == 3, colors), - const SizedBox(height: 8), - ], - ), - ), - ), - ); - } - - Widget _headerOption(String label, int? level, bool isActive, ColorScheme colors) { - final sizes = {null: 15.0, 1: 22.0, 2: 18.0, 3: 15.0}; - return InkWell( - onTap: () { - Navigator.pop(context); - if (_controller == null) return; - if (level == null) { - _controller!.formatSelection(quill.Attribute.header); - } else { - _controller!.formatSelection(quill.Attribute.clone(quill.Attribute.header, level)); - } - }, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), - child: Row( - children: [ - Expanded( - child: Text(label, style: TextStyle( - fontSize: sizes[level] ?? 15, - fontWeight: level == null ? FontWeight.w400 : FontWeight.w600, - color: isActive ? colors.primary : colors.onSurface, - )), - ), - if (isActive) - Icon(Icons.check, size: 18, color: colors.primary), - ], - ), - ), - ); - } - - // ─── 保存对话框 ───────────────────────────────── - - void _showSaveDialog(NotePlusProvider provider) { - final colors = Theme.of(context).colorScheme; - showDialog( - context: context, - builder: (ctx) => AlertDialog( - backgroundColor: colors.surface, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), - title: Text('未保存的更改', - style: TextStyle(fontSize: 17, fontWeight: FontWeight.w600, - color: colors.onSurface)), - content: Text('是否保存当前文档?', - style: TextStyle(fontSize: 14, - color: colors.onSurface.withValues(alpha: 0.6))), - actions: [ - TextButton( - onPressed: () { Navigator.pop(ctx); Navigator.pop(context); }, - child: Text('不保存', style: TextStyle(color: colors.error)), - ), - TextButton( - onPressed: () => Navigator.pop(ctx), - child: Text('取消', - style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6))), - ), - ElevatedButton( - onPressed: () { Navigator.pop(ctx); _save(provider); Navigator.pop(context); }, - style: ElevatedButton.styleFrom( - backgroundColor: colors.primary, foregroundColor: colors.onPrimary, - elevation: 0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), - ), - child: const Text('保存'), - ), - ], - ), - ); - } -} diff --git a/lib/pages/note_plus/note_plus_tab_page.dart b/lib/pages/note_plus/note_plus_tab_page.dart deleted file mode 100644 index 21abdeb..0000000 --- a/lib/pages/note_plus/note_plus_tab_page.dart +++ /dev/null @@ -1,657 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:expandable/expandable.dart'; -import '../../providers/note_plus_provider.dart'; -import '../../models/note_plus_models.dart'; - -/// Note Plus 树形文件列表页 -class NotePlusTabPage extends StatefulWidget { - const NotePlusTabPage({super.key}); - - @override - State createState() => _NotePlusTabPageState(); -} - -class _NotePlusTabPageState extends State { - String? _selectedDocId; - String? _dragOverNodeId; // 拖到节点上(成为子节点) - _DropPosition? _dropPos; // 拖到节点之间(排序) - - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) { - context.read().loadDocuments(); - }); - } - - @override - Widget build(BuildContext context) { - final colors = Theme.of(context).colorScheme; - return Column( - children: [ - _buildHeader(colors), - Expanded(child: _buildTree(colors)), - ], - ); - } - - Widget _buildHeader(ColorScheme colors) { - return Container( - padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), - child: Row(children: [ - Icon(Icons.edit_note, size: 18, color: colors.onSurface.withValues(alpha: 0.5)), - const SizedBox(width: 8), - Text('Note Plus', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, - color: colors.onSurface.withValues(alpha: 0.7))), - const Spacer(), - GestureDetector( - onTap: () async { - final provider = context.read(); - final doc = await provider.createDocument(); - if (!mounted) return; - Navigator.pushNamed(context, '/note-plus-form', arguments: doc.id); - }, - child: Icon(Icons.add, size: 20, - color: colors.onSurface.withValues(alpha: 0.45)), - ), - const SizedBox(width: 14), - GestureDetector( - onTap: () => _showRecycleBin(context), - child: Icon(Icons.delete_outline, size: 18, - color: colors.onSurface.withValues(alpha: 0.35)), - ), - ]), - ); - } - - Widget _buildTree(ColorScheme colors) { - return Consumer( - builder: (context, provider, _) { - if (provider.documents.isEmpty) return _buildEmptyState(colors); - - final roots = provider.documents - .where((d) => d.parentId.isEmpty) - .toList() - ..sort((a, b) => a.sortIndex.compareTo(b.sortIndex)); - - if (roots.isEmpty) return _buildEmptyState(colors); - - return ExpandableTheme( - data: const ExpandableThemeData( - useInkWell: false, - animationDuration: Duration(milliseconds: 200), - headerAlignment: ExpandablePanelHeaderAlignment.center, - tapBodyToExpand: false, tapBodyToCollapse: false, - tapHeaderToExpand: false, iconPadding: EdgeInsets.zero, hasIcon: false, - ), - child: RefreshIndicator( - onRefresh: () => provider.loadDocuments(), - child: ListView( - padding: const EdgeInsets.only(left: 8, right: 8, bottom: 40), - children: _buildChildrenList(roots, 0, colors), - ), - ), - ); - }, - ); - } - - /// 构建子节点列表,节点之间插入 drop zone(共 n+1 个间隙) - List _buildChildrenList(List docs, int depth, ColorScheme colors) { - final widgets = []; - final parentId = docs.isNotEmpty ? docs.first.parentId : ''; - for (int i = 0; i < docs.length; i++) { - // 节点前的间隙 - widgets.add(_buildReorderGap(parentId, i, depth, colors)); - // 节点本身 - widgets.add(_buildTreeNode(docs[i], depth, colors)); - } - // 末尾间隙 - if (docs.isNotEmpty) { - widgets.add(_buildReorderGap(parentId, docs.length, depth, colors)); - } - return widgets; - } - - /// 排序间隙 drop zone - Widget _buildReorderGap(String parentId, int insertIndex, int depth, ColorScheme colors) { - final indent = 28.0 + depth * 18.0; - final isTarget = _dropPos != null && - _dropPos!.parentId == parentId && - _dropPos!.index == insertIndex; - - return DragTarget( - onWillAcceptWithDetails: (details) { - final draggedId = details.data; - final provider = context.read(); - final dragged = provider.documents.where((d) => d.id == draggedId).firstOrNull; - if (dragged == null) return false; - // 防止拖到自己的子孙上 - if (parentId.isNotEmpty && - _isDescendantOf(provider.documents, childId: parentId, ancestorId: draggedId)) { - return false; - } - setState(() => _dropPos = _DropPosition(parentId, insertIndex)); - return true; - }, - onLeave: (_) { - if (_dropPos?.parentId == parentId && _dropPos?.index == insertIndex) { - setState(() => _dropPos = null); - } - }, - onAcceptWithDetails: (details) { - setState(() { _dropPos = null; _dragOverNodeId = null; }); - _handleDrop(details.data, parentId, insertIndex); - }, - builder: (_, __, ___) { - return AnimatedContainer( - duration: const Duration(milliseconds: 150), - height: isTarget ? 36 : 6, - margin: EdgeInsets.only(left: indent, right: 16), - decoration: isTarget - ? BoxDecoration( - color: colors.primary.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(6), - border: Border.all(color: colors.primary.withValues(alpha: 0.4), width: 1), - ) - : null, - child: isTarget - ? Center( - child: Text('放置到此处', - style: TextStyle(fontSize: 11, - color: colors.primary.withValues(alpha: 0.7))), - ) - : null, - ); - }, - ); - } - - /// 递归构建单个树节点 - Widget _buildTreeNode(NotePlusDocument doc, int depth, ColorScheme colors) { - final provider = context.watch(); - final children = provider.documents - .where((d) => d.parentId == doc.id) - .toList() - ..sort((a, b) => a.sortIndex.compareTo(b.sortIndex)); - final hasChildren = children.isNotEmpty; - final isSelected = _selectedDocId == doc.id; - final isDropTarget = _dragOverNodeId == doc.id; - - final nodeContent = _buildNodeContent(doc, depth, - hasChildren: hasChildren, isSelected: isSelected, - isDropTarget: isDropTarget, colors: colors); - - // 可拖拽包装 - final draggable = LongPressDraggable( - data: doc.id, - delay: const Duration(milliseconds: 300), - feedback: Material( - elevation: 4, borderRadius: BorderRadius.circular(6), - child: Container( - width: MediaQuery.of(context).size.width - 48, - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - decoration: BoxDecoration( - color: colors.surfaceContainerHigh, - borderRadius: BorderRadius.circular(6), - ), - child: Row(children: [ - Icon(Icons.description_outlined, size: 14, - color: colors.onSurface.withValues(alpha: 0.5)), - const SizedBox(width: 8), - Expanded(child: Text(doc.title.isEmpty ? '无标题' : doc.title, - style: TextStyle(fontSize: 12, color: colors.onSurface), - maxLines: 1, overflow: TextOverflow.ellipsis)), - ]), - ), - ), - childWhenDragging: Opacity(opacity: 0.3, child: nodeContent), - child: DragTarget( - onWillAcceptWithDetails: (details) { - final draggedId = details.data; - if (draggedId == doc.id) return false; - if (_isDescendantOf(provider.documents, childId: doc.id, ancestorId: draggedId)) { - return false; - } - setState(() => _dragOverNodeId = doc.id); - return true; - }, - onLeave: (_) { - if (_dragOverNodeId == doc.id) setState(() => _dragOverNodeId = null); - }, - onAcceptWithDetails: (details) { - setState(() { _dragOverNodeId = null; _dropPos = null; }); - // 拖到节点上 → 成为该节点的子节点 - provider.moveDocument(details.data, doc.id); - }, - builder: (_, __, ___) => nodeContent, - ), - ); - - // 组装:可展开节点带子树,叶子节点直接返回 - if (hasChildren) { - return ExpandableNotifier( - initialExpanded: true, - child: Builder( - builder: (innerCtx) { - // 给 _buildNodeContent 传入 expandContext 用于展开/折叠 - final nodeWithExpand = _buildNodeContent(doc, depth, - hasChildren: true, isSelected: isSelected, - isDropTarget: isDropTarget, expandContext: innerCtx, colors: colors); - - final draggableWithExpand = LongPressDraggable( - data: doc.id, delay: const Duration(milliseconds: 300), - feedback: Material( - elevation: 4, borderRadius: BorderRadius.circular(6), - child: Container( - width: MediaQuery.of(context).size.width - 48, - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - decoration: BoxDecoration( - color: colors.surfaceContainerHigh, - borderRadius: BorderRadius.circular(6), - ), - child: Row(children: [ - Icon(Icons.description_outlined, size: 14, - color: colors.onSurface.withValues(alpha: 0.5)), - const SizedBox(width: 8), - Expanded(child: Text(doc.title.isEmpty ? '无标题' : doc.title, - style: TextStyle(fontSize: 12, color: colors.onSurface), - maxLines: 1, overflow: TextOverflow.ellipsis)), - ]), - ), - ), - childWhenDragging: Opacity(opacity: 0.3, child: nodeWithExpand), - child: DragTarget( - onWillAcceptWithDetails: (details) { - final draggedId = details.data; - if (draggedId == doc.id) return false; - if (_isDescendantOf(provider.documents, childId: doc.id, ancestorId: draggedId)) { - return false; - } - setState(() => _dragOverNodeId = doc.id); - return true; - }, - onLeave: (_) { - if (_dragOverNodeId == doc.id) setState(() => _dragOverNodeId = null); - }, - onAcceptWithDetails: (details) { - setState(() { _dragOverNodeId = null; _dropPos = null; }); - provider.moveDocument(details.data, doc.id); - }, - builder: (_, __, ___) => nodeWithExpand, - ), - ); - - return Column(children: [ - draggableWithExpand, - Expandable( - collapsed: const SizedBox(), - expanded: Column(children: _buildChildrenList(children, depth + 1, colors)), - ), - ]); - }, - ), - ); - } - - return draggable; - } - - /// 节点内容(标题行) - Widget _buildNodeContent(NotePlusDocument doc, int depth, - {required bool hasChildren, required bool isSelected, - required bool isDropTarget, BuildContext? expandContext, - required ColorScheme colors}) { - final indent = 12.0 + depth * 18.0; - - return Material( - color: Colors.transparent, - child: InkWell( - onTap: () { - setState(() => _selectedDocId = doc.id); - Navigator.pushNamed(context, '/note-plus-form', arguments: doc.id); - }, - onLongPress: () => _showDocActions(doc), - borderRadius: BorderRadius.circular(4), - child: Container( - height: 30, - padding: EdgeInsets.only(left: indent, right: 8), - decoration: BoxDecoration( - color: isSelected - ? colors.primary.withValues(alpha: 0.08) - : isDropTarget - ? colors.primary.withValues(alpha: 0.12) - : null, - borderRadius: BorderRadius.circular(4), - border: isDropTarget - ? Border.all(color: colors.primary.withValues(alpha: 0.4), width: 1) - : null, - ), - child: Row(children: [ - // 展开/折叠箭头 - if (hasChildren) - SizedBox( - width: 20, height: 20, - child: InkWell( - onTap: () { - if (expandContext != null) { - ExpandableController.of(expandContext, - rebuildOnChange: false, required: true) - ?.toggle(); - } - }, - child: expandContext != null - ? ExpandableIcon( - theme: ExpandableThemeData( - expandIcon: Icons.keyboard_arrow_right, - collapseIcon: Icons.keyboard_arrow_down, - iconColor: colors.onSurface.withValues(alpha: 0.4), - iconSize: 14, iconPadding: EdgeInsets.zero, hasIcon: false, - ), - ) - : Icon(Icons.keyboard_arrow_down, size: 14, - color: colors.onSurface.withValues(alpha: 0.4)), - ), - ) - else - const SizedBox(width: 20), - // 图标 - Icon(_getDocIcon(doc), size: 14, - color: isSelected ? colors.primary : colors.onSurface.withValues(alpha: 0.4)), - const SizedBox(width: 6), - // 标题 - Expanded( - child: Text(doc.title.isEmpty ? '无标题' : doc.title, - style: TextStyle( - fontSize: 13, - fontWeight: isSelected ? FontWeight.w500 : FontWeight.w400, - color: isSelected ? colors.primary : colors.onSurface.withValues(alpha: 0.75), - ), maxLines: 1, overflow: TextOverflow.ellipsis), - ), - // 添加子文档 - SizedBox( - width: 20, height: 20, - child: InkWell( - onTap: () => _createChildDoc(doc.id), - borderRadius: BorderRadius.circular(4), - child: Icon(Icons.add, size: 13, - color: colors.onSurface.withValues(alpha: 0.25)), - ), - ), - const SizedBox(width: 2), - // 删除 - SizedBox( - width: 20, height: 20, - child: InkWell( - onTap: () => _showDeleteDialog(doc.id), - borderRadius: BorderRadius.circular(4), - child: Icon(Icons.delete_outline, size: 13, - color: colors.onSurface.withValues(alpha: 0.25)), - ), - ), - ]), - ), - ), - ); - } - - // ─── 拖放处理 ───────────────────────────────────── - - void _handleDrop(String draggedId, String targetParentId, int insertIndex) { - final provider = context.read(); - provider.moveDocumentTo(draggedId, targetParentId, insertIndex); - } - - // ─── 工具 ───────────────────────────────────── - - bool _isDescendantOf(List docs, - {required String childId, required String ancestorId}) { - if (childId.isEmpty) return false; - if (childId == ancestorId) return true; // 自身也算(防止拖到自己下面) - var current = docs.where((d) => d.id == childId).firstOrNull; - while (current != null) { - if (current.parentId == ancestorId) return true; - current = docs.where((d) => d.id == current!.parentId).firstOrNull; - } - return false; - } - - IconData _getDocIcon(NotePlusDocument doc) { - if (doc.blocks.isEmpty) return Icons.description_outlined; - switch (doc.blocks.first.type) { - case NoteBlockType.heading1: - case NoteBlockType.heading2: - case NoteBlockType.heading3: - return Icons.title; - case NoteBlockType.checklist: - return Icons.check_box_outlined; - case NoteBlockType.codeBlock: - return Icons.code; - case NoteBlockType.quote: - return Icons.format_quote; - default: - return Icons.description_outlined; - } - } - - // ─── 操作 ───────────────────────────────────── - - Future _createChildDoc(String parentId) async { - final provider = context.read(); - final doc = await provider.createDocument(parentId: parentId); - if (!mounted) return; - Navigator.pushNamed(context, '/note-plus-form', arguments: doc.id); - } - - void _showDocActions(NotePlusDocument doc) { - final colors = Theme.of(context).colorScheme; - showModalBottomSheet( - context: context, - backgroundColor: colors.surface, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(16)), - ), - builder: (ctx) => SafeArea( - child: Column(mainAxisSize: MainAxisSize.min, children: [ - Container(width: 36, height: 4, - margin: const EdgeInsets.only(top: 10, bottom: 16), - decoration: BoxDecoration( - color: colors.onSurface.withValues(alpha: 0.15), - borderRadius: BorderRadius.circular(2), - ), - ), - ListTile( - leading: Icon(Icons.subdirectory_arrow_right, size: 20, - color: colors.onSurface.withValues(alpha: 0.6)), - title: const Text('新建子文档', style: TextStyle(fontSize: 14)), - onTap: () { Navigator.pop(ctx); _createChildDoc(doc.id); }, - ), - ListTile( - leading: Icon(Icons.edit_outlined, size: 20, - color: colors.onSurface.withValues(alpha: 0.6)), - title: const Text('重命名', style: TextStyle(fontSize: 14)), - onTap: () { Navigator.pop(ctx); _showRenameDialog(doc); }, - ), - ListTile( - leading: Icon(Icons.delete_outline, size: 20, color: colors.error), - title: Text('删除', style: TextStyle(fontSize: 14, color: colors.error)), - subtitle: Text('子文档将提升到上一级', - style: TextStyle(fontSize: 11, - color: colors.onSurface.withValues(alpha: 0.4))), - onTap: () { Navigator.pop(ctx); _showDeleteDialog(doc.id); }, - ), - const SizedBox(height: 8), - ]), - ), - ); - } - - void _showRenameDialog(NotePlusDocument doc) { - final colors = Theme.of(context).colorScheme; - final controller = TextEditingController(text: doc.title); - showDialog( - context: context, - builder: (ctx) => AlertDialog( - backgroundColor: colors.surface, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - title: Text('重命名', style: TextStyle(fontSize: 17, fontWeight: FontWeight.w600, - color: colors.onSurface)), - content: TextField( - controller: controller, autofocus: true, - decoration: InputDecoration(hintText: '输入标题', - border: OutlineInputBorder(borderRadius: BorderRadius.circular(8))), - onSubmitted: (_) { - context.read().renameDocument(doc.id, controller.text); - Navigator.pop(ctx); - }, - ), - actions: [ - TextButton(onPressed: () => Navigator.pop(ctx), - child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6)))), - ElevatedButton( - onPressed: () { - context.read().renameDocument(doc.id, controller.text); - Navigator.pop(ctx); - }, - style: ElevatedButton.styleFrom( - backgroundColor: colors.primary, foregroundColor: colors.onPrimary, - elevation: 0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), - ), - child: const Text('确定'), - ), - ], - ), - ); - } - - void _showDeleteDialog(String id) { - final colors = Theme.of(context).colorScheme; - showDialog( - context: context, - builder: (ctx) => AlertDialog( - backgroundColor: colors.surface, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - title: Text('删除文档', style: TextStyle(fontSize: 17, fontWeight: FontWeight.w600, - color: colors.onSurface)), - content: Text('子文档将自动提升到上一级。可在回收站恢复。', - style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6))), - actions: [ - TextButton(onPressed: () => Navigator.pop(ctx), - child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6)))), - ElevatedButton( - onPressed: () { - Navigator.pop(ctx); - context.read().deleteDocument(id); - }, - style: ElevatedButton.styleFrom( - backgroundColor: colors.error, foregroundColor: colors.onError, - elevation: 0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), - ), - child: const Text('删除'), - ), - ], - ), - ); - } - - void _showRecycleBin(BuildContext context) { - final colors = Theme.of(context).colorScheme; - final provider = context.read(); - showModalBottomSheet( - context: context, backgroundColor: colors.surface, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(16)), - ), - isScrollControlled: true, - builder: (ctx) => DraggableScrollableSheet( - initialChildSize: 0.5, minChildSize: 0.3, maxChildSize: 0.8, expand: false, - builder: (ctx, scrollCtrl) { - return FutureBuilder>( - future: provider.getDeletedDocuments(), - builder: (context, snapshot) { - final deleted = snapshot.data ?? []; - return Column(children: [ - Container(width: 36, height: 4, - margin: const EdgeInsets.only(top: 10, bottom: 14), - decoration: BoxDecoration( - color: colors.onSurface.withValues(alpha: 0.15), - borderRadius: BorderRadius.circular(2), - ), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Row(children: [ - Text('回收站', style: TextStyle(fontSize: 15, - fontWeight: FontWeight.w600, color: colors.onSurface)), - const Spacer(), - Text('${deleted.length} 个文档', style: TextStyle(fontSize: 12, - color: colors.onSurface.withValues(alpha: 0.4))), - ]), - ), - const SizedBox(height: 12), - Expanded( - child: deleted.isEmpty - ? Center(child: Text('回收站为空', - style: TextStyle(fontSize: 13, - color: colors.onSurface.withValues(alpha: 0.35)))) - : ListView.builder( - controller: scrollCtrl, itemCount: deleted.length, - itemBuilder: (context, i) { - final doc = deleted[i]; - return ListTile( - dense: true, - leading: Icon(Icons.description_outlined, size: 18, - color: colors.onSurface.withValues(alpha: 0.4)), - title: Text(doc.title.isEmpty ? '无标题' : doc.title, - style: const TextStyle(fontSize: 13)), - trailing: Row(mainAxisSize: MainAxisSize.min, children: [ - IconButton( - icon: Icon(Icons.restore, size: 18, color: colors.primary), - onPressed: () { provider.restoreDocument(doc.id); Navigator.pop(ctx); }, - ), - IconButton( - icon: Icon(Icons.delete_forever, size: 18, color: colors.error), - onPressed: () { provider.permanentDeleteDocument(doc.id); Navigator.pop(ctx); }, - ), - ]), - ); - }, - ), - ), - ]); - }, - ); - }, - ), - ); - } - - Widget _buildEmptyState(ColorScheme colors) { - return Center( - child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ - Container(width: 64, height: 64, - decoration: BoxDecoration( - color: colors.surfaceContainerHighest, - borderRadius: BorderRadius.circular(16), - ), - child: Icon(Icons.edit_note, size: 32, - color: colors.onSurface.withValues(alpha: 0.2)), - ), - const SizedBox(height: 16), - Text('暂无文档', style: TextStyle(fontSize: 14, - color: colors.onSurface.withValues(alpha: 0.4))), - const SizedBox(height: 6), - Text('点击右上角 + 创建', style: TextStyle(fontSize: 12, - color: colors.onSurface.withValues(alpha: 0.25))), - ]), - ); - } -} - -/// 拖放位置记录 -class _DropPosition { - final String parentId; - final int index; - _DropPosition(this.parentId, this.index); -} diff --git a/lib/pages/profile_page.dart b/lib/pages/profile_page.dart index 29ebcb1..f05c9c1 100644 --- a/lib/pages/profile_page.dart +++ b/lib/pages/profile_page.dart @@ -5,7 +5,6 @@ import 'package:image_picker/image_picker.dart'; import 'package:path_provider/path_provider.dart'; import 'package:path/path.dart' as path; import 'package:provider/provider.dart'; -import 'package:webview_flutter/webview_flutter.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:permission_handler/permission_handler.dart'; import '../main.dart' show routeObserver; @@ -2321,7 +2320,6 @@ class _FeatureSettingsPageState extends State { bool _showMovieTab = true; bool _showBookTab = true; bool _showNoteTab = true; - bool _showNotePlusTab = false; int _defaultTabIndex = 0; // 侧边栏 @@ -2346,7 +2344,6 @@ class _FeatureSettingsPageState extends State { _showMovieTab = _userPrefs.showMovieTab; _showBookTab = _userPrefs.showBookTab; _showNoteTab = _userPrefs.showNoteTab; - _showNotePlusTab = _userPrefs.showNotePlusTab; _defaultTabIndex = _userPrefs.defaultMainTabIndex; _showHeatmap = _userPrefs.showSidebarHeatmap; _showRecent = _userPrefs.showSidebarRecent; @@ -2428,10 +2425,6 @@ class _FeatureSettingsPageState extends State { }); } - Future _toggleNotePlusTab(bool value) async { - await _userPrefs.setShowNotePlusTab(value); - setState(() => _showNotePlusTab = value); - } @override Widget build(BuildContext context) { @@ -2473,9 +2466,6 @@ class _FeatureSettingsPageState extends State { indent: 24, endIndent: 24, color: colors.outlineVariant), - _buildSwitchItem(Icons.edit_note, 'Note Plus', '块编辑器,支持富文本文档', - _showNotePlusTab, _toggleNotePlusTab), - // ── 侧边栏:信息模块 ── _buildSectionHeader('侧边栏 · 信息模块'), _buildSwitchItem( @@ -3001,50 +2991,3 @@ class _LayoutSettingsPageState extends State { ); } } - -/// WebView 页面 -class WebViewPage extends StatefulWidget { - final String url; - const WebViewPage({super.key, required this.url}); - - @override - State createState() => _WebViewPageState(); -} - -class _WebViewPageState extends State { - late final WebViewController _controller; - bool _isLoading = true; - - @override - void initState() { - super.initState(); - _controller = WebViewController() - ..setJavaScriptMode(JavaScriptMode.unrestricted) - ..setNavigationDelegate(NavigationDelegate( - onPageStarted: (_) => setState(() => _isLoading = true), - onPageFinished: (_) => setState(() => _isLoading = false), - onWebResourceError: (_) => setState(() => _isLoading = false), - )) - ..loadRequest(Uri.parse(widget.url)); - } - - @override - Widget build(BuildContext context) { - final colors = Theme.of(context).colorScheme; - return Scaffold( - backgroundColor: colors.surface, - appBar: AppBar(title: const Text(''), actions: [ - IconButton( - icon: const Icon(Icons.refresh), - onPressed: () => _controller.reload()) - ]), - body: Stack(children: [ - WebViewWidget(controller: _controller), - if (_isLoading) - Center( - child: CircularProgressIndicator( - color: colors.onSurface.withValues(alpha: 0.4))) - ]), - ); - } -} diff --git a/lib/pages/recycle_bin_page.dart b/lib/pages/recycle_bin_page.dart index bdf50ac..545254d 100644 --- a/lib/pages/recycle_bin_page.dart +++ b/lib/pages/recycle_bin_page.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../providers/app_provider.dart'; import '../models/data_models.dart'; -import '../models/note_plus_models.dart'; import '../utils/toast_util.dart'; /// 回收站页面 @@ -13,7 +12,7 @@ class RecycleBinPage extends StatefulWidget { State createState() => _RecycleBinPageState(); } -enum _ItemType { movie, book, note, movieReview, bookReview, bookExcerpt, notePlus } +enum _ItemType { movie, book, note, movieReview, bookReview, bookExcerpt } class _DeletedItem { final _ItemType type; @@ -71,13 +70,6 @@ class _DeletedItem { icon = Icons.format_quote_outlined, typeLabel = '书摘'; - _DeletedItem.notePlus(NotePlusDocument d) - : type = _ItemType.notePlus, - id = d.id, - title = d.title.isNotEmpty ? d.title : '未命名文档', - subtitle = '删除于 ${d.updatedAt.year}.${d.updatedAt.month.toString().padLeft(2, '0')}.${d.updatedAt.day.toString().padLeft(2, '0')}', - icon = Icons.edit_note_outlined, - typeLabel = '高级笔记'; } class _RecycleBinPageState extends State { @@ -103,7 +95,6 @@ class _RecycleBinPageState extends State { final movieReviews = await provider.getDeletedMovieReviews(); final bookReviews = await provider.getDeletedBookReviews(); final bookExcerpts = await provider.getDeletedBookExcerpts(); - final notePlusDocs = await provider.getDeletedNotePlusDocs(); if (!mounted) return; setState(() { _allItems = [ @@ -113,7 +104,6 @@ class _RecycleBinPageState extends State { for (final r in movieReviews) _DeletedItem.movieReview(r), for (final r in bookReviews) _DeletedItem.bookReview(r), for (final e in bookExcerpts) _DeletedItem.bookExcerpt(e), - for (final d in notePlusDocs) _DeletedItem.notePlus(d), ]; _isLoading = false; }); @@ -188,7 +178,6 @@ class _RecycleBinPageState extends State { _filterChip('影评', _ItemType.movieReview), _filterChip('书评', _ItemType.bookReview), _filterChip('书摘', _ItemType.bookExcerpt), - _filterChip('高级笔记', _ItemType.notePlus), ], ), ); @@ -411,9 +400,6 @@ class _RecycleBinPageState extends State { case _ItemType.bookExcerpt: await provider.restoreBookExcerpt(item.id); if (mounted) ToastUtil.show(context, '书摘已恢复'); - case _ItemType.notePlus: - await provider.restoreNotePlusDoc(item.id); - if (mounted) ToastUtil.show(context, '高级笔记已恢复'); } _loadDeletedItems(); } @@ -435,8 +421,6 @@ class _RecycleBinPageState extends State { await provider.permanentDeleteBookReview(item.id); case _ItemType.bookExcerpt: await provider.permanentDeleteBookExcerpt(item.id); - case _ItemType.notePlus: - await provider.permanentDeleteNotePlusDoc(item.id); } _loadDeletedItems(); if (mounted) ToastUtil.show(context, '已彻底删除'); diff --git a/lib/providers/app_provider.dart b/lib/providers/app_provider.dart index 0272629..c92ba9c 100644 --- a/lib/providers/app_provider.dart +++ b/lib/providers/app_provider.dart @@ -1,7 +1,6 @@ import 'dart:collection'; import 'package:flutter/material.dart'; import '../models/data_models.dart'; -import '../models/note_plus_models.dart'; import '../utils/movie/movie_dao.dart'; import '../utils/book/book_dao.dart'; import '../utils/note/note_dao.dart'; @@ -9,7 +8,6 @@ import '../utils/movie/movie_review_dao.dart'; import '../utils/movie/movie_poster_dao.dart'; import '../utils/book/book_review_dao.dart'; import '../utils/book/book_excerpt_dao.dart'; -import '../utils/note_plus/note_plus_dao.dart'; import '../utils/tag/tag_dao.dart'; import '../utils/database_helper.dart'; import '../utils/image_path_helper.dart'; @@ -27,7 +25,6 @@ class AppProvider extends ChangeNotifier { final MoviePosterDao _posterDao = MoviePosterDao(); final BookReviewDao _bookReviewDao = BookReviewDao(); final BookExcerptDao _bookExcerptDao = BookExcerptDao(); - final NotePlusDao _notePlusDao = NotePlusDao(); final TagDao _tagDao = TagDao(); // 数据列表 List _movies = []; @@ -560,7 +557,6 @@ class AppProvider extends ChangeNotifier { final deletedMovieReviews = await getDeletedMovieReviews(); final deletedBookReviews = await getDeletedBookReviews(); final deletedBookExcerpts = await getDeletedBookExcerpts(); - final deletedNotePlusDocs = await _notePlusDao.getDeleted(); for (final movie in deletedMovies) { await permanentDeleteMovie(movie.id); @@ -580,9 +576,6 @@ class AppProvider extends ChangeNotifier { for (final excerpt in deletedBookExcerpts) { await _bookExcerptDao.permanentDeleteExcerpt(excerpt.id); } - for (final doc in deletedNotePlusDocs) { - await _notePlusDao.permanentDelete(doc.id); - } await loadMovies(); await loadBooks(); @@ -629,19 +622,6 @@ class AppProvider extends ChangeNotifier { await _bookExcerptDao.permanentDeleteExcerpt(id); } - // ========== Note Plus 回收站 ========== - - Future> getDeletedNotePlusDocs() async { - return await _notePlusDao.getDeleted(); - } - - Future restoreNotePlusDoc(String id) async { - await _notePlusDao.restore(id); - } - - Future permanentDeleteNotePlusDoc(String id) async { - await _notePlusDao.permanentDelete(id); - } // ========== 标签管理方法 ========== diff --git a/lib/providers/note_plus_provider.dart b/lib/providers/note_plus_provider.dart deleted file mode 100644 index 1d21fce..0000000 --- a/lib/providers/note_plus_provider.dart +++ /dev/null @@ -1,459 +0,0 @@ -import 'package:flutter/foundation.dart'; -import '../models/note_plus_models.dart'; -import '../utils/note_plus/note_plus_dao.dart'; - -/// Note Plus 块编辑器状态管理 -class NotePlusProvider extends ChangeNotifier { - final NotePlusDao _dao = NotePlusDao(); - - List _documents = []; - NotePlusDocument? _currentDocument; - List _blocks = []; - int _focusedBlockIndex = 0; - bool _isDirty = false; - - // Undo/Redo - final List> _undoStack = []; - final List> _redoStack = []; - static const int _maxUndoSize = 50; - - // Getters - List get documents => _documents; - NotePlusDocument? get currentDocument => _currentDocument; - List get blocks => _blocks; - int get focusedBlockIndex => _focusedBlockIndex; - bool get isDirty => _isDirty; - bool get canUndo => _undoStack.isNotEmpty; - bool get canRedo => _redoStack.isNotEmpty; - - // ========== 文档 CRUD ========== - - Future loadDocuments() async { - _documents = await _dao.getAll(); - notifyListeners(); - } - - Future loadDocumentById(String id) async { - _currentDocument = await _dao.getById(id); - if (_currentDocument != null) { - _blocks = _currentDocument!.blocks.map((b) => b.deepCopy()).toList(); - _focusedBlockIndex = 0; - _isDirty = false; - _undoStack.clear(); - _redoStack.clear(); - } - notifyListeners(); - } - - Future saveDocument({String? deltaJson}) async { - if (_currentDocument == null) return; - final now = DateTime.now(); - // 直接构造,避免 copyWith 的 null 歧义 - _currentDocument = NotePlusDocument( - id: _currentDocument!.id, - title: _currentDocument!.title, - parentId: _currentDocument!.parentId, - sortIndex: _currentDocument!.sortIndex, - blocks: deltaJson == null ? List.from(_blocks) : _currentDocument!.blocks, - blocksJson: deltaJson, - tags: _currentDocument!.tags, - images: _currentDocument!.images, - createdAt: _currentDocument!.createdAt, - updatedAt: now, - isDeleted: _currentDocument!.isDeleted, - ); - await _dao.update(_currentDocument!); - final idx = _documents.indexWhere((d) => d.id == _currentDocument!.id); - if (idx >= 0) _documents[idx] = _currentDocument!; - _isDirty = false; - notifyListeners(); - } - - /// 保存 Quill Delta JSON 作为文档内容(flutter_quill 模式) - void saveDeltaJson(String deltaJson) { - if (_currentDocument == null) return; - _currentDocument = NotePlusDocument( - id: _currentDocument!.id, - title: _currentDocument!.title, - parentId: _currentDocument!.parentId, - sortIndex: _currentDocument!.sortIndex, - blocksJson: deltaJson, - tags: _currentDocument!.tags, - images: _currentDocument!.images, - createdAt: _currentDocument!.createdAt, - updatedAt: DateTime.now(), - isDeleted: _currentDocument!.isDeleted, - ); - _isDirty = true; - } - - Future createDocument({String title = '', String parentId = ''}) async { - final doc = NotePlusDocument(title: title, parentId: parentId); - await _dao.insert(doc); - await loadDocuments(); - return doc; - } - - /// 移动文档到指定父级的指定位置(原子操作,同时处理重排序) - Future moveDocumentTo(String docId, String newParentId, int insertIndex) async { - final idx = _documents.indexWhere((d) => d.id == docId); - if (idx < 0) return; - - final doc = _documents[idx]; - final oldParentId = doc.parentId; - - // 获取目标父级下的同级文档(不含自身) - final siblings = _documents - .where((d) => d.parentId == newParentId && d.id != docId) - .toList() - ..sort((a, b) => a.sortIndex.compareTo(b.sortIndex)); - - // 钳制插入位置 - var pos = insertIndex.clamp(0, siblings.length); - - // 如果是同级移动,且拖到自己的后面,需要减 1(因为移除自己后索引会偏移) - if (oldParentId == newParentId) { - final oldIndex = siblings.indexWhere((d) => d.sortIndex > doc.sortIndex); - if (oldIndex >= 0 && pos > oldIndex) { - pos = (pos - 1).clamp(0, siblings.length); - } - } - - // 插入到指定位置 - siblings.insert(pos, doc); - - // 批量更新 sortIndex 和 parentId - for (int i = 0; i < siblings.length; i++) { - final s = siblings[i]; - final needsUpdate = s.sortIndex != i || s.id == docId && doc.parentId != newParentId; - if (needsUpdate) { - final updated = s.copyWith( - parentId: newParentId, - sortIndex: i, - updatedAt: DateTime.now(), - ); - await _dao.update(updated); - final uIdx = _documents.indexWhere((d) => d.id == updated.id); - if (uIdx >= 0) _documents[uIdx] = updated; - } - } - - notifyListeners(); - } - - /// 旧接口兼容:仅移动到新父级(放到末尾) - Future moveDocument(String docId, String newParentId) async { - final siblings = _documents.where((d) => d.parentId == newParentId && d.id != docId).toList(); - await moveDocumentTo(docId, newParentId, siblings.length); - } - - /// 删除文档时,将其子文档提升到祖父节点 - Future deleteDocument(String id) async { - final idx = _documents.indexWhere((d) => d.id == id); - if (idx >= 0) { - final parentId = _documents[idx].parentId; - // 将子文档的 parent_id 改为被删文档的 parent_id - final children = _documents.where((d) => d.parentId == id).toList(); - for (final child in children) { - final updated = child.copyWith(parentId: parentId, updatedAt: DateTime.now()); - await _dao.update(updated); - } - } - await _dao.delete(id); - await loadDocuments(); - } - - Future restoreDocument(String id) async { - await _dao.restore(id); - await loadDocuments(); - } - - Future permanentDeleteDocument(String id) async { - await _dao.permanentDelete(id); - await loadDocuments(); - } - - Future> getDeletedDocuments() async { - return await _dao.getDeleted(); - } - - Future searchDocuments(String query) async { - _documents = await _dao.search(query); - notifyListeners(); - } - - // ========== 文档属性 ========== - - void setTitle(String title) { - if (_currentDocument == null) return; - _currentDocument = _currentDocument!.copyWith(title: title); - _isDirty = true; - notifyListeners(); - } - - /// 重命名文档(在文档列表中调用) - Future renameDocument(String id, String newTitle) async { - final idx = _documents.indexWhere((d) => d.id == id); - if (idx < 0) return; - final doc = _documents[idx].copyWith(title: newTitle, updatedAt: DateTime.now()); - await _dao.update(doc); - _documents[idx] = doc; - notifyListeners(); - } - - // ========== Block 操作 ========== - - void setFocusedBlock(int index) { - if (index < 0 || index >= _blocks.length) return; - _focusedBlockIndex = index; - notifyListeners(); - } - - /// 添加 block(在指定位置之后) - void addBlock(int afterIndex, NoteBlock block) { - _pushUndo(); - final insertAt = afterIndex + 1; - if (insertAt >= _blocks.length) { - _blocks.add(block); - } else { - _blocks.insert(insertAt, block); - } - _focusedBlockIndex = insertAt; - _isDirty = true; - notifyListeners(); - } - - /// 移除 block - void removeBlock(int index) { - if (_blocks.length <= 1 || index < 0 || index >= _blocks.length) return; - _pushUndo(); - _blocks.removeAt(index); - if (_focusedBlockIndex >= _blocks.length) { - _focusedBlockIndex = _blocks.length - 1; - } - _isDirty = true; - notifyListeners(); - } - - /// 更新 block 内容(不触发 notify,编辑时高频调用) - void updateBlockSilent(int index, NoteBlock block) { - if (index < 0 || index >= _blocks.length) return; - _blocks[index] = block; - _isDirty = true; - } - - /// 更新 block 并通知 - void updateBlock(int index, NoteBlock block) { - updateBlockSilent(index, block); - notifyListeners(); - } - - /// 拖拽移动 block - void moveBlock(int oldIndex, int newIndex) { - if (oldIndex == newIndex) return; - _pushUndo(); - final block = _blocks.removeAt(oldIndex); - final insertAt = newIndex > oldIndex ? newIndex - 1 : newIndex; - _blocks.insert(insertAt, block); - _focusedBlockIndex = insertAt; - _isDirty = true; - notifyListeners(); - } - - /// 转换 block 类型 - void convertBlockType(int index, NoteBlockType newType) { - if (index < 0 || index >= _blocks.length) return; - _pushUndo(); - _blocks[index] = _blocks[index].copyWith(type: newType); - if (newType == NoteBlockType.divider) { - _blocks[index] = _blocks[index].copyWith(text: ''); - } - _isDirty = true; - notifyListeners(); - } - - /// Enter 分割 block - /// 返回新 block 的索引 - int splitBlock(int index, int splitPosition) { - if (index < 0 || index >= _blocks.length) return index; - _pushUndo(); - - final current = _blocks[index]; - final textBefore = current.text.substring(0, splitPosition); - final textAfter = current.text.substring(splitPosition); - - // 裁剪当前 block 的格式 - final beforeFormatting = _clipFormatting(current.formatting, 0, splitPosition); - - // 新 block 继承类型(heading 降为 paragraph) - var newType = current.type; - if (newType == NoteBlockType.heading1 || - newType == NoteBlockType.heading2 || - newType == NoteBlockType.heading3) { - newType = NoteBlockType.paragraph; - } - - // 平移新 block 的格式 - final afterFormatting = _shiftFormatting(current.formatting, splitPosition, -splitPosition); - - // 更新当前 block - _blocks[index] = current.copyWith( - text: textBefore, - formatting: beforeFormatting, - ); - - // 插入新 block - final newBlock = NoteBlock( - type: newType, - text: textAfter, - formatting: afterFormatting, - ); - final insertAt = index + 1; - _blocks.insert(insertAt, newBlock); - _focusedBlockIndex = insertAt; - _isDirty = true; - notifyListeners(); - return insertAt; - } - - /// Backspace 合并到前一个 block - /// 返回合并后的光标位置 - int mergeWithPrevious(int index) { - if (index <= 0 || index >= _blocks.length) return 0; - _pushUndo(); - - final prev = _blocks[index - 1]; - final current = _blocks[index]; - final cursorPos = prev.text.length; - - // 合并文本 - final mergedText = prev.text + current.text; - - // 合并格式:平移当前 block 的格式 - final shiftedFormatting = _shiftFormatting(current.formatting, 0, cursorPos); - final mergedFormatting = [...prev.formatting, ...shiftedFormatting]; - - // 更新前一个 block - _blocks[index - 1] = prev.copyWith( - text: mergedText, - formatting: mergedFormatting, - ); - - // 移除当前 block - _blocks.removeAt(index); - _focusedBlockIndex = index - 1; - _isDirty = true; - notifyListeners(); - return cursorPos; - } - - /// 切换待办状态 - void toggleChecklist(int index) { - if (index < 0 || index >= _blocks.length) return; - final block = _blocks[index]; - if (block.type != NoteBlockType.checklist) return; - _pushUndo(); - - final checked = block.metadata['checked'] == true; - _blocks[index] = block.copyWith( - metadata: {...block.metadata, 'checked': !checked}, - ); - _isDirty = true; - notifyListeners(); - } - - /// 应用内联格式 - void applyFormat(int blockIndex, InlineFormatType format, int start, int end) { - if (blockIndex < 0 || blockIndex >= _blocks.length) return; - if (start >= end) return; - - final block = _blocks[blockIndex]; - final formatting = List.from(block.formatting); - - // 查找是否已有重叠的同格式 span - bool found = false; - for (int i = 0; i < formatting.length; i++) { - final span = formatting[i]; - if (span.start <= start && span.end >= end && span.formats.contains(format)) { - // 移除格式 - final newFormats = Set.from(span.formats)..remove(format); - if (newFormats.isEmpty) { - formatting.removeAt(i); - } else { - formatting[i] = span.copyWith(formats: newFormats); - } - found = true; - break; - } - } - - if (!found) { - formatting.add(InlineFormatSpan(start: start, end: end, formats: {format})); - } - - _blocks[blockIndex] = block.copyWith(formatting: formatting); - _isDirty = true; - notifyListeners(); - } - - // ========== Undo/Redo ========== - - void _pushUndo() { - _undoStack.add(_blocks.map((b) => b.deepCopy()).toList()); - if (_undoStack.length > _maxUndoSize) { - _undoStack.removeAt(0); - } - _redoStack.clear(); - } - - void undo() { - if (_undoStack.isEmpty) return; - _redoStack.add(_blocks.map((b) => b.deepCopy()).toList()); - _blocks = _undoStack.removeLast(); - if (_focusedBlockIndex >= _blocks.length) { - _focusedBlockIndex = _blocks.length - 1; - } - _isDirty = true; - notifyListeners(); - } - - void redo() { - if (_redoStack.isEmpty) return; - _undoStack.add(_blocks.map((b) => b.deepCopy()).toList()); - _blocks = _redoStack.removeLast(); - if (_focusedBlockIndex >= _blocks.length) { - _focusedBlockIndex = _blocks.length - 1; - } - _isDirty = true; - notifyListeners(); - } - - // ========== 格式辅助 ========== - - /// 裁剪格式区间到 [clipStart, clipEnd) - List _clipFormatting( - List spans, int clipStart, int clipEnd) { - return spans - .where((s) => s.end > clipStart && s.start < clipEnd) - .map((s) => InlineFormatSpan( - start: s.start < clipStart ? clipStart : s.start, - end: s.end > clipEnd ? clipEnd : s.end, - formats: Set.from(s.formats), - )) - .toList(); - } - - /// 平移格式区间(从 fromPos 之后的 span 偏移 offset) - List _shiftFormatting( - List spans, int fromPos, int offset) { - return spans - .where((s) => s.end > fromPos) - .map((s) => InlineFormatSpan( - start: (s.start < fromPos ? fromPos : s.start) + offset, - end: s.end + offset, - formats: Set.from(s.formats), - )) - .where((s) => s.start >= 0 && s.end > s.start) - .toList(); - } -} diff --git a/lib/utils/app_router.dart b/lib/utils/app_router.dart index 3f603c0..e7e5827 100644 --- a/lib/utils/app_router.dart +++ b/lib/utils/app_router.dart @@ -10,8 +10,6 @@ import '../pages/movies/movie_detail_page.dart'; import '../pages/book/book_detail_page.dart'; import '../pages/note/note_detail_page.dart'; import '../pages/movies/douban_webview_page.dart'; -import '../pages/note_plus/note_plus_form_page.dart'; -import '../pages/note_plus/note_plus_detail_page.dart'; /// 路由生成器 class AppRouter { @@ -73,20 +71,6 @@ class AppRouter { } return SlideUpPageRoute(page: DoubanWebViewPage(url: url)); - case '/note-plus-form': - final id = settings.arguments is String ? settings.arguments as String : null; - if (id == null) { - return _buildUnknownRoute(settings.name); - } - return SlideUpPageRoute(page: NotePlusFormPage(documentId: id)); - - case '/note-plus-detail': - final id = settings.arguments is String ? settings.arguments as String : null; - if (id == null) { - return _buildUnknownRoute(settings.name); - } - return SlideUpPageRoute(page: NotePlusDetailPage(documentId: id)); - default: return _buildUnknownRoute(settings.name); } diff --git a/lib/utils/database_helper.dart b/lib/utils/database_helper.dart index 61acdad..e0f3fa0 100644 --- a/lib/utils/database_helper.dart +++ b/lib/utils/database_helper.dart @@ -72,7 +72,7 @@ class DatabaseHelper { return await openDatabase( path, - version: 27, + version: 28, onCreate: _createDB, onUpgrade: _onUpgrade, ); @@ -174,10 +174,10 @@ class DatabaseHelper { await db.execute('ALTER TABLE tags ADD COLUMN is_hidden INTEGER NOT NULL DEFAULT 0'); } if (oldVersion < 18) { - await _createNotePlusTable(db); + // note_plus table creation removed (feature dropped in v28) } if (oldVersion < 19) { - await _createNotePlusTable(db); + // note_plus table creation removed (feature dropped in v28) } if (oldVersion < 20) { // 确保 note_plus 表有 parent_id 列(从旧版 folder 迁移) @@ -251,6 +251,10 @@ class DatabaseHelper { if (oldVersion < 27) { await _upgradeBooksTableV27(db); } + if (oldVersion < 28) { + // 移除 Note Plus 功能,删除 note_plus 表 + await db.execute('DROP TABLE IF EXISTS note_plus'); + } } /// 升级books表到V27(添加阅读始末日期字段) @@ -765,9 +769,6 @@ class DatabaseHelper { ) '''); - // Note Plus 块编辑器文档表 - await _createNotePlusTable(db); - // EPUB 阅读器书籍表 await db.execute(''' CREATE TABLE IF NOT EXISTS reader_books ( @@ -804,24 +805,6 @@ class DatabaseHelper { '''); } - /// 创建 Note Plus 文档表 - Future _createNotePlusTable(Database db) async { - await db.execute(''' - CREATE TABLE IF NOT EXISTS note_plus ( - id TEXT PRIMARY KEY, - title TEXT DEFAULT '', - parent_id TEXT DEFAULT '', - sort_index INTEGER DEFAULT 0, - blocks_json TEXT NOT NULL, - tags TEXT, - images TEXT, - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL, - is_deleted INTEGER DEFAULT 0 - ) - '''); - } - // 关闭数据库 Future close() async { if (_database != null) { diff --git a/lib/utils/note_plus/note_plus_dao.dart b/lib/utils/note_plus/note_plus_dao.dart deleted file mode 100644 index f636de7..0000000 --- a/lib/utils/note_plus/note_plus_dao.dart +++ /dev/null @@ -1,134 +0,0 @@ -import 'package:flutter/foundation.dart'; -import '../../models/note_plus_models.dart'; -import '../database_helper.dart'; - -/// Note Plus 块文档数据访问对象 -class NotePlusDao { - final DatabaseHelper _dbHelper = DatabaseHelper.instance; - - Future _wrap(String op, Future Function() fn) async { - try { - return await fn(); - } catch (e) { - debugPrint('[NotePlusDao] $op error: $e'); - rethrow; - } - } - - // 获取所有未删除的文档 - Future> getAll() => _wrap('getAll', () async { - final db = await _dbHelper.database; - final List> maps = await db.query( - 'note_plus', - where: 'is_deleted = ?', - whereArgs: [0], - orderBy: 'sort_index ASC, updated_at DESC', - ); - return List.generate(maps.length, (i) => NotePlusDocument.fromJson(maps[i])); - }); - - // 分页查询 - Future> getPaged({int limit = 20, int offset = 0}) => - _wrap('getPaged', () async { - final db = await _dbHelper.database; - final maps = await db.query('note_plus', where: 'is_deleted = 0', - orderBy: 'sort_index ASC, updated_at DESC', limit: limit, offset: offset); - return List.generate(maps.length, (i) => NotePlusDocument.fromJson(maps[i])); - }); - - // 根据ID获取 - Future getById(String id) => _wrap('getById', () async { - final db = await _dbHelper.database; - final List> maps = await db.query( - 'note_plus', - where: 'id = ? AND is_deleted = ?', - whereArgs: [id, 0], - ); - if (maps.isEmpty) return null; - return NotePlusDocument.fromJson(maps.first); - }); - - // 插入 - Future insert(NotePlusDocument doc) => _wrap('insert', () async { - final db = await _dbHelper.database; - return await db.insert('note_plus', doc.toJson()); - }); - - // 更新 - Future update(NotePlusDocument doc) => _wrap('update', () async { - final db = await _dbHelper.database; - return await db.update( - 'note_plus', - doc.toJson(), - where: 'id = ?', - whereArgs: [doc.id], - ); - }); - - // 软删除 - Future delete(String id) => _wrap('delete', () async { - final db = await _dbHelper.database; - return await db.update( - 'note_plus', - {'is_deleted': 1, 'updated_at': DateTime.now().toIso8601String()}, - where: 'id = ?', - whereArgs: [id], - ); - }); - - // ========== 回收站 ========== - - Future> getDeleted() => _wrap('getDeleted', () async { - final db = await _dbHelper.database; - final List> maps = await db.query( - 'note_plus', - where: 'is_deleted = ?', - whereArgs: [1], - orderBy: 'sort_index ASC, updated_at DESC', - ); - return List.generate(maps.length, (i) => NotePlusDocument.fromJson(maps[i])); - }); - - Future restore(String id) => _wrap('restore', () async { - final db = await _dbHelper.database; - return await db.update( - 'note_plus', - {'is_deleted': 0, 'updated_at': DateTime.now().toIso8601String()}, - where: 'id = ?', - whereArgs: [id], - ); - }); - - Future permanentDelete(String id) => _wrap('permanentDelete', () async { - final db = await _dbHelper.database; - return await db.delete( - 'note_plus', - where: 'id = ?', - whereArgs: [id], - ); - }); - - // 搜索 - Future> search(String query) => _wrap('search', () async { - final db = await _dbHelper.database; - final List> maps = await db.query( - 'note_plus', - where: '(title LIKE ? OR blocks_json LIKE ? OR tags LIKE ?) AND is_deleted = ?', - whereArgs: ['%$query%', '%$query%', '%$query%', 0], - orderBy: 'sort_index ASC, updated_at DESC', - ); - return List.generate(maps.length, (i) => NotePlusDocument.fromJson(maps[i])); - }); - - // 根据标签筛选 - Future> getByTag(String tag) => _wrap('getByTag', () async { - final db = await _dbHelper.database; - final List> maps = await db.query( - 'note_plus', - where: 'tags LIKE ? AND is_deleted = ?', - whereArgs: ['%$tag%', 0], - orderBy: 'sort_index ASC, updated_at DESC', - ); - return List.generate(maps.length, (i) => NotePlusDocument.fromJson(maps[i])); - }); -} diff --git a/lib/utils/sync/backup_service.dart b/lib/utils/sync/backup_service.dart index 399d127..f1995f4 100644 --- a/lib/utils/sync/backup_service.dart +++ b/lib/utils/sync/backup_service.dart @@ -33,7 +33,6 @@ class BackupService { final tags = await db.query('tags'); final readerBooks = await db.query('reader_books'); final bookAnnotations = await db.query('book_annotations'); - final notePlus = await db.query('note_plus'); // 收集图片路径 final imagePaths = {}; @@ -92,7 +91,6 @@ class BackupService { 'tags': tags, 'reader_books': readerBooks, 'book_annotations': bookAnnotations, - 'note_plus': notePlus, }, }; @@ -317,7 +315,6 @@ class BackupService { final tagsCols = await _getTableColumns(db, 'tags'); final readerBooksCols = await _getTableColumns(db, 'reader_books'); final bookAnnotationsCols = await _getTableColumns(db, 'book_annotations'); - final notePlusCols = await _getTableColumns(db, 'note_plus'); await db.transaction((txn) async { await txn.delete('movie_reviews'); @@ -329,7 +326,6 @@ class BackupService { await txn.delete('books'); await txn.delete('notes'); await txn.delete('reader_books'); - await txn.delete('note_plus'); await txn.delete('tags'); if (data.containsKey('movies')) { @@ -379,11 +375,6 @@ class BackupService { await txn.insert('book_annotations', _convertToDbMapSafe(a, bookAnnotationsCols)); } } - if (data.containsKey('note_plus')) { - for (final np in data['note_plus'] as List) { - await txn.insert('note_plus', _convertToDbMapSafe(np, notePlusCols)); - } - } if (data.containsKey('tags')) { for (final t in data['tags'] as List) { final map = _convertToDbMapSafe(t, tagsCols); @@ -459,7 +450,6 @@ class BackupService { final tagsCols = await _getTableColumns(db, 'tags'); final readerBooksCols = await _getTableColumns(db, 'reader_books'); final bookAnnotationsCols = await _getTableColumns(db, 'book_annotations'); - final notePlusCols = await _getTableColumns(db, 'note_plus'); await db.transaction((txn) async { await txn.delete('movie_reviews'); @@ -471,7 +461,6 @@ class BackupService { await txn.delete('books'); await txn.delete('notes'); await txn.delete('reader_books'); - await txn.delete('note_plus'); await txn.delete('tags'); // 修复: 之前漏删 tags 表 if (data.containsKey('movies')) { @@ -521,11 +510,6 @@ class BackupService { await txn.insert('book_annotations', _convertToDbMapSafe(a, bookAnnotationsCols)); } } - if (data.containsKey('note_plus')) { - for (final np in data['note_plus'] as List) { - await txn.insert('note_plus', _convertToDbMapSafe(np, notePlusCols)); - } - } if (data.containsKey('tags')) { for (final t in data['tags'] as List) { final map = _convertToDbMapSafe(t, tagsCols); @@ -621,7 +605,6 @@ class BackupService { if (data.containsKey('tags')) stats['标签'] = (data['tags'] as List).length; if (data.containsKey('reader_books')) stats['阅读'] = (data['reader_books'] as List).length; if (data.containsKey('book_annotations')) stats['批注'] = (data['book_annotations'] as List).length; - if (data.containsKey('note_plus')) stats['高级笔记'] = (data['note_plus'] as List).length; if (imageCount > 0) stats['图片'] = imageCount; return stats; } diff --git a/lib/utils/user_prefs.dart b/lib/utils/user_prefs.dart index 7ad5533..0b540ab 100644 --- a/lib/utils/user_prefs.dart +++ b/lib/utils/user_prefs.dart @@ -104,10 +104,6 @@ class UserPrefs { bool get showNoteTab => prefs.getBool('showNoteTab') ?? true; Future setShowNoteTab(bool value) => prefs.setBool('showNoteTab', value); - /// 是否显示 Note Plus 标签(默认关闭) - bool get showNotePlusTab => prefs.getBool('showNotePlusTab') ?? false; - Future setShowNotePlusTab(bool value) => prefs.setBool('showNotePlusTab', value); - /// 默认启动标签 (0: 影视, 1: 阅读, 2: 笔记) int get defaultMainTabIndex => prefs.getInt('defaultMainTabIndex') ?? 0; Future setDefaultMainTabIndex(int value) => prefs.setInt('defaultMainTabIndex', value); diff --git a/lib/widgets/note_plus/block_toolbar.dart b/lib/widgets/note_plus/block_toolbar.dart deleted file mode 100644 index e3e5020..0000000 --- a/lib/widgets/note_plus/block_toolbar.dart +++ /dev/null @@ -1,230 +0,0 @@ -import 'package:flutter/material.dart'; -import '../../models/note_plus_models.dart'; - -/// AppFlowy 风格的格式化工具栏 -/// -/// 样式参考 AppFlowy tool_bar.dart: -/// - 高度 36px(iconSize 18 * 2) -/// - 背景 #f2f2f2 -/// - 按钮 18px 图标,~32px 宽 -/// - 切换态:#00bcf0 背景 + 白色图标 -class BlockToolbar extends StatelessWidget { - final NoteBlockType currentBlockType; - final Set activeFormats; - final void Function(InlineFormatType) onFormatToggle; - final void Function(NoteBlockType) onBlockTypeChange; - final VoidCallback onUndo; - final VoidCallback onRedo; - - const BlockToolbar({ - super.key, - required this.currentBlockType, - required this.activeFormats, - required this.onFormatToggle, - required this.onBlockTypeChange, - required this.onUndo, - required this.onRedo, - }); - - static const double _iconSize = 18; - static const double _buttonWidth = 32; - static const Color _bgColor = Color(0xFFF2F2F2); - static const Color _toggledColor = Color(0xFF00BCF0); - - @override - Widget build(BuildContext context) { - final colors = Theme.of(context).colorScheme; - - return Container( - height: 36, - padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4), - decoration: const BoxDecoration(color: _bgColor), - child: ListView( - scrollDirection: Axis.horizontal, - children: [ - // 撤销/重做 - _iconButton(Icons.undo_outlined, onUndo, false), - _iconButton(Icons.redo_outlined, onRedo, false), - _divider(), - // 内联格式 - _iconButton(Icons.format_bold, () => onFormatToggle(InlineFormatType.bold), - activeFormats.contains(InlineFormatType.bold)), - _iconButton(Icons.format_italic, () => onFormatToggle(InlineFormatType.italic), - activeFormats.contains(InlineFormatType.italic)), - _iconButton(Icons.format_underlined, () => onFormatToggle(InlineFormatType.underline), - activeFormats.contains(InlineFormatType.underline)), - _iconButton(Icons.format_strikethrough, () => onFormatToggle(InlineFormatType.strikethrough), - activeFormats.contains(InlineFormatType.strikethrough)), - _divider(), - // 标题 - _headingButton('H1', NoteBlockType.heading1), - _headingButton('H2', NoteBlockType.heading2), - _headingButton('H3', NoteBlockType.heading3), - _divider(), - // 列表 - _iconButton(Icons.format_list_numbered, () => onBlockTypeChange(NoteBlockType.numberedList), - currentBlockType == NoteBlockType.numberedList), - _iconButton(Icons.format_list_bulleted, () => onBlockTypeChange(NoteBlockType.bulletList), - currentBlockType == NoteBlockType.bulletList), - _iconButton(Icons.check_box_outlined, () => onBlockTypeChange(NoteBlockType.checklist), - currentBlockType == NoteBlockType.checklist), - _divider(), - // 代码/引用 - _iconButton(Icons.code, () => onFormatToggle(InlineFormatType.inlineCode), - activeFormats.contains(InlineFormatType.inlineCode)), - _iconButton(Icons.format_quote, () => onBlockTypeChange(NoteBlockType.quote), - currentBlockType == NoteBlockType.quote), - // 块类型选择 - _divider(), - _blockTypeSelector(context, colors), - ], - ), - ); - } - - Widget _iconButton(IconData icon, VoidCallback onTap, bool isToggled) { - return SizedBox( - width: _buttonWidth, - child: Material( - color: isToggled ? _toggledColor : Colors.transparent, - borderRadius: BorderRadius.circular(2), - child: InkWell( - onTap: onTap, - borderRadius: BorderRadius.circular(2), - hoverColor: isToggled ? _toggledColor : const Color(0xFFE0E0E0), - splashColor: Colors.transparent, - highlightColor: Colors.transparent, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4), - child: Icon(icon, - size: _iconSize, - color: isToggled ? Colors.white : const Color(0xFF333333)), - ), - ), - ), - ); - } - - Widget _headingButton(String label, NoteBlockType type) { - final isToggled = currentBlockType == type; - return SizedBox( - width: _buttonWidth, - child: Material( - color: isToggled ? _toggledColor : Colors.transparent, - borderRadius: BorderRadius.circular(2), - child: InkWell( - onTap: () => onBlockTypeChange(type), - borderRadius: BorderRadius.circular(2), - hoverColor: isToggled ? _toggledColor : const Color(0xFFE0E0E0), - splashColor: Colors.transparent, - highlightColor: Colors.transparent, - child: Center( - child: Text(label, - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w600, - color: isToggled ? Colors.white : const Color(0xFF333333), - )), - ), - ), - ), - ); - } - - Widget _divider() { - return Container( - width: 1, - margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 4), - color: const Color(0xFFE0E0E0), - ); - } - - Widget _blockTypeSelector(BuildContext context, ColorScheme colors) { - return GestureDetector( - onTap: () => _showPicker(context), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text(currentBlockType.label, - style: const TextStyle(fontSize: 12, color: Color(0xFF4F4F4F))), - const SizedBox(width: 4), - const Icon(Icons.arrow_drop_down, size: 16, color: Color(0xFF828282)), - ], - ), - ), - ); - } - - void _showPicker(BuildContext context) { - final colors = Theme.of(context).colorScheme; - showModalBottomSheet( - context: context, - backgroundColor: colors.surface, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(16)), - ), - builder: (ctx) { - return SafeArea( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - width: 36, height: 4, - margin: const EdgeInsets.only(top: 10, bottom: 16), - decoration: BoxDecoration( - color: colors.onSurface.withValues(alpha: 0.15), - borderRadius: BorderRadius.circular(2), - ), - ), - ...NoteBlockType.values.map((type) { - final isSelected = type == currentBlockType; - return ListTile( - dense: true, - leading: Icon(_getBlockIcon(type), size: 20, - color: isSelected ? _toggledColor : colors.onSurface.withValues(alpha: 0.5)), - title: Text(type.label, - style: TextStyle(fontSize: 14, - fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400, - color: isSelected ? _toggledColor : colors.onSurface)), - trailing: isSelected - ? const Icon(Icons.check, size: 18, color: _toggledColor) - : null, - onTap: () { - Navigator.pop(ctx); - onBlockTypeChange(type); - }, - ); - }), - const SizedBox(height: 8), - ], - ), - ); - }, - ); - } - - IconData _getBlockIcon(NoteBlockType type) { - switch (type) { - case NoteBlockType.paragraph: - return Icons.notes; - case NoteBlockType.heading1: - case NoteBlockType.heading2: - case NoteBlockType.heading3: - return Icons.title; - case NoteBlockType.bulletList: - return Icons.format_list_bulleted; - case NoteBlockType.numberedList: - return Icons.format_list_numbered; - case NoteBlockType.checklist: - return Icons.check_box_outlined; - case NoteBlockType.quote: - return Icons.format_quote; - case NoteBlockType.codeBlock: - return Icons.code; - case NoteBlockType.divider: - return Icons.horizontal_rule; - } - } -} diff --git a/lib/widgets/note_plus/note_plus_block_widget.dart b/lib/widgets/note_plus/note_plus_block_widget.dart deleted file mode 100644 index 7aa2132..0000000 --- a/lib/widgets/note_plus/note_plus_block_widget.dart +++ /dev/null @@ -1,385 +0,0 @@ -import 'package:flutter/material.dart'; -import '../../models/note_plus_models.dart'; - -/// 单个块的渲染组件 -/// -/// 负责根据 block type 渲染不同 UI。 -/// 焦点 block 使用 TextEditingController 编辑,非焦点 block 渲染为静态 Text。 -class NotePlusBlockWidget extends StatelessWidget { - final NoteBlock block; - final int index; - final int numberedIndex; // 有序列表序号 - final bool isFocused; - final TextEditingController? controller; - final FocusNode? focusNode; - final VoidCallback onTap; - final VoidCallback? onToggleChecklist; - final void Function(String)? onTextChanged; - final void Function(TextSelection)? onSelectionChanged; - - const NotePlusBlockWidget({ - super.key, - required this.block, - required this.index, - this.numberedIndex = 1, - required this.isFocused, - this.controller, - this.focusNode, - required this.onTap, - this.onToggleChecklist, - this.onTextChanged, - this.onSelectionChanged, - }); - - @override - Widget build(BuildContext context) { - final colors = Theme.of(context).colorScheme; - - if (block.type == NoteBlockType.divider) { - return _buildDivider(colors); - } - - return GestureDetector( - onTap: onTap, - behavior: HitTestBehavior.opaque, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 左侧标记区 - _buildPrefix(colors), - const SizedBox(width: 4), - // 内容区 - Expanded(child: _buildContent(context, colors)), - ], - ), - ); - } - - /// 块类型前缀标记 - Widget _buildPrefix(ColorScheme colors) { - switch (block.type) { - case NoteBlockType.bulletList: - return Padding( - padding: const EdgeInsets.only(top: 12), - child: Container( - width: 6, - height: 6, - decoration: BoxDecoration( - color: colors.onSurface.withValues(alpha: 0.5), - shape: BoxShape.circle, - ), - ), - ); - case NoteBlockType.numberedList: - return Padding( - padding: const EdgeInsets.only(top: 10), - child: SizedBox( - width: 24, - child: Text( - '$numberedIndex.', - style: TextStyle( - fontSize: 14, - color: colors.onSurface.withValues(alpha: 0.5), - ), - ), - ), - ); - case NoteBlockType.checklist: - return Padding( - padding: const EdgeInsets.only(top: 6), - child: GestureDetector( - onTap: onToggleChecklist, - child: Icon( - block.metadata['checked'] == true - ? Icons.check_box - : Icons.check_box_outline_blank, - size: 20, - color: block.metadata['checked'] == true - ? colors.primary - : colors.onSurface.withValues(alpha: 0.4), - ), - ), - ); - case NoteBlockType.quote: - // AppFlowy: 4px wide grey.shade300 left border - return Container( - width: 4, - margin: const EdgeInsets.only(top: 6, bottom: 6, right: 12), - decoration: BoxDecoration( - color: Colors.grey.shade300, - borderRadius: BorderRadius.circular(2), - ), - ); - default: - return const SizedBox(width: 0); - } - } - - /// 块内容 - Widget _buildContent(BuildContext context, ColorScheme colors) { - final style = _getTextStyle(colors); - - Widget content; - if (!isFocused) { - content = Padding( - padding: _getContentPadding(), - child: block.text.isEmpty - ? Text(_getPlaceholder(), style: style.copyWith( - color: colors.onSurface.withValues(alpha: 0.25))) - : _buildRichText(block.text, style, colors), - ); - } else { - content = _buildEditable(style, colors); - } - - // AppFlowy code block: grey.shade50 background, 2px border radius - if (block.type == NoteBlockType.codeBlock) { - return Container( - decoration: BoxDecoration( - color: Colors.grey.shade50, - borderRadius: BorderRadius.circular(2), - ), - child: content, - ); - } - - return content; - } - - Widget _buildEditable(TextStyle style, ColorScheme colors) { - return TextField( - controller: controller, - focusNode: focusNode, - style: style, - maxLines: null, - minLines: 1, - decoration: InputDecoration( - hintText: _getPlaceholder(), - hintStyle: style.copyWith( - color: colors.onSurface.withValues(alpha: 0.25)), - border: InputBorder.none, - contentPadding: _getContentPadding(), - isDense: true, - ), - onChanged: onTextChanged, - // selection handler 通过 controller listener 处理 - ); - } - - Widget _buildRichText(String text, TextStyle style, ColorScheme colors) { - if (block.formatting.isEmpty) { - return Text(text, style: style); - } - - final spans = _buildTextSpans(text, style, colors); - return Text.rich( - TextSpan(children: spans), - maxLines: null, - ); - } - - List _buildTextSpans( - String text, TextStyle baseStyle, ColorScheme colors) { - // 收集所有格式边界 - final events = <_FormatEvent>[]; - for (final span in block.formatting) { - if (span.start < text.length) { - events.add(_FormatEvent(span.start, true, span.formats)); - events.add(_FormatEvent( - span.end > text.length ? text.length : span.end, false, span.formats)); - } - } - events.sort((a, b) { - if (a.pos != b.pos) return a.pos.compareTo(b.pos); - // 关闭优先于打开 - if (a.isStart && !b.isStart) return 1; - if (!a.isStart && b.isStart) return -1; - return 0; - }); - - if (events.isEmpty) return [TextSpan(text: text, style: baseStyle)]; - - final spans = []; - int lastPos = 0; - final activeFormats = {}; - - for (final event in events) { - if (event.pos > lastPos) { - spans.add(TextSpan( - text: text.substring(lastPos, event.pos), - style: _applyFormats(baseStyle, activeFormats, colors), - )); - } - if (event.isStart) { - activeFormats.addAll(event.formats); - } else { - activeFormats.removeAll(event.formats); - } - lastPos = event.pos; - } - - if (lastPos < text.length) { - spans.add(TextSpan( - text: text.substring(lastPos), - style: _applyFormats(baseStyle, activeFormats, colors), - )); - } - - return spans; - } - - TextStyle _applyFormats( - TextStyle style, Set formats, ColorScheme colors) { - if (formats.isEmpty) return style; - return style.copyWith( - fontWeight: formats.contains(InlineFormatType.bold) - ? FontWeight.bold - : null, - fontStyle: formats.contains(InlineFormatType.italic) - ? FontStyle.italic - : null, - decoration: _getTextDecoration(formats), - fontFamily: formats.contains(InlineFormatType.inlineCode) - ? 'monospace' - : null, - fontSize: formats.contains(InlineFormatType.inlineCode) ? 13 : null, - color: formats.contains(InlineFormatType.inlineCode) - ? Colors.blue.shade900.withValues(alpha: 0.9) - : null, - backgroundColor: formats.contains(InlineFormatType.inlineCode) - ? Colors.grey.shade50 - : null, - ); - } - - TextDecoration? _getTextDecoration(Set formats) { - final decorations = []; - if (formats.contains(InlineFormatType.underline)) { - decorations.add(TextDecoration.underline); - } - if (formats.contains(InlineFormatType.strikethrough)) { - decorations.add(TextDecoration.lineThrough); - } - if (decorations.isEmpty) return null; - return TextDecoration.combine(decorations); - } - - TextStyle _getTextStyle(ColorScheme colors) { - // AppFlowy: 18px, w300, height 1.3, letter-spacing 0.6 - final base = TextStyle( - color: colors.onSurface, - fontSize: 18, - fontWeight: FontWeight.w300, - height: 1.3, - letterSpacing: 0.6, - ); - switch (block.type) { - case NoteBlockType.heading1: - // AppFlowy H1: 34px, w300, height 1.15, 70% opacity - return base.copyWith( - fontSize: 34, fontWeight: FontWeight.w300, height: 1.15, - letterSpacing: 0, - color: colors.onSurface.withValues(alpha: 0.7)); - case NoteBlockType.heading2: - // AppFlowy H2: 24px, w400, height 1.15, 70% opacity - return base.copyWith( - fontSize: 24, fontWeight: FontWeight.w400, height: 1.15, - letterSpacing: 0, - color: colors.onSurface.withValues(alpha: 0.7)); - case NoteBlockType.heading3: - // AppFlowy H3: 20px, w500, height 1.25, 70% opacity - return base.copyWith( - fontSize: 20, fontWeight: FontWeight.w500, height: 1.25, - letterSpacing: 0, - color: colors.onSurface.withValues(alpha: 0.7)); - case NoteBlockType.quote: - // AppFlowy quote: 60% opacity text - return base.copyWith( - color: colors.onSurface.withValues(alpha: 0.6)); - case NoteBlockType.codeBlock: - // AppFlowy code: 13px, blue.shade900 at 90%, height 1.15, monospace - return base.copyWith( - fontSize: 13, - fontWeight: FontWeight.w400, - fontFamily: 'monospace', - height: 1.15, - letterSpacing: 0, - color: Colors.blue.shade900.withValues(alpha: 0.9), - ); - case NoteBlockType.checklist: - return base.copyWith( - decoration: block.metadata['checked'] == true - ? TextDecoration.lineThrough - : null, - color: block.metadata['checked'] == true - ? colors.onSurface.withValues(alpha: 0.4) - : null, - ); - default: - return base; - } - } - - EdgeInsets _getContentPadding() { - switch (block.type) { - case NoteBlockType.heading1: - return const EdgeInsets.only(top: 16); - case NoteBlockType.heading2: - return const EdgeInsets.only(top: 8); - case NoteBlockType.heading3: - return const EdgeInsets.only(top: 8); - case NoteBlockType.codeBlock: - // AppFlowy code: grey.shade50 background, no inner padding - return const EdgeInsets.symmetric(vertical: 8, horizontal: 12); - case NoteBlockType.quote: - // AppFlowy quote: 6px top, 2px bottom inner padding - return const EdgeInsets.only(top: 6, bottom: 2); - default: - // AppFlowy paragraph: 10px top spacing - return const EdgeInsets.only(top: 10); - } - } - - String _getPlaceholder() { - switch (block.type) { - case NoteBlockType.heading1: - return '标题1'; - case NoteBlockType.heading2: - return '标题2'; - case NoteBlockType.heading3: - return '标题3'; - case NoteBlockType.bulletList: - return '列表项'; - case NoteBlockType.numberedList: - return '列表项'; - case NoteBlockType.checklist: - return '待办事项'; - case NoteBlockType.quote: - return '引用'; - case NoteBlockType.codeBlock: - return '代码'; - default: - return '输入文字,或输入 / 打开菜单'; - } - } - - Widget _buildDivider(ColorScheme colors) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 12), - child: GestureDetector( - onTap: onTap, - child: Container( - height: 1, - color: colors.outlineVariant, - ), - ), - ); - } -} - -class _FormatEvent { - final int pos; - final bool isStart; - final Set formats; - _FormatEvent(this.pos, this.isStart, this.formats); -} diff --git a/lib/widgets/note_plus/note_plus_editor.dart b/lib/widgets/note_plus/note_plus_editor.dart deleted file mode 100644 index 39e1097..0000000 --- a/lib/widgets/note_plus/note_plus_editor.dart +++ /dev/null @@ -1,308 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:provider/provider.dart'; -import '../../models/note_plus_models.dart'; -import '../../providers/note_plus_provider.dart'; -import 'note_plus_block_widget.dart'; -import 'slash_command_menu.dart'; - -/// Note Plus 块编辑器 -/// -/// 管理 block 列表、焦点切换、键盘事件处理。 -/// 使用 ReorderableListView 支持拖拽排序。 -class NotePlusEditor extends StatefulWidget { - const NotePlusEditor({super.key}); - - @override - State createState() => _NotePlusEditorState(); -} - -class _NotePlusEditorState extends State { - final ScrollController _scrollController = ScrollController(); - final Map _controllers = {}; - final Map _focusNodes = {}; - OverlayEntry? _slashMenuOverlay; - - @override - void dispose() { - _scrollController.dispose(); - for (final c in _controllers.values) { - c.dispose(); - } - for (final fn in _focusNodes.values) { - fn.dispose(); - } - _removeSlashMenu(); - super.dispose(); - } - - TextEditingController _getController(NoteBlock block) { - return _controllers.putIfAbsent(block.id, () { - final c = TextEditingController(text: block.text); - c.addListener(() => _onControllerChanged(block)); - return c; - }); - } - - FocusNode _getFocusNode(NoteBlock block, int index) { - return _focusNodes.putIfAbsent(block.id, () { - final fn = FocusNode(onKeyEvent: (node, event) => _handleKeyEvent(index, event)); - fn.addListener(() { - if (fn.hasFocus) { - context.read().setFocusedBlock(index); - } - }); - return fn; - }); - } - - /// 清理已删除 block 对应的控制器和焦点节点 - void _cleanupStaleEntries(List currentBlocks) { - final currentIds = currentBlocks.map((b) => b.id).toSet(); - final staleIds = _controllers.keys.where((id) => !currentIds.contains(id)).toList(); - for (final id in staleIds) { - _controllers.remove(id)?.dispose(); - } - final staleFocusIds = _focusNodes.keys.where((id) => !currentIds.contains(id)).toList(); - for (final id in staleFocusIds) { - _focusNodes.remove(id)?.dispose(); - } - } - - void _onControllerChanged(NoteBlock block) { - final provider = context.read(); - final idx = provider.blocks.indexWhere((b) => b.id == block.id); - if (idx < 0) return; - - final text = _controllers[block.id]!.text; - - // 更新 block 文本(静默,不 rebuild) - provider.updateBlockSilent( - idx, - provider.blocks[idx].copyWith(text: text), - ); - - // 检测斜杠命令 - if (text == '/' || (text.startsWith('/') && text.length > 1)) { - _showSlashMenu(idx, text); - } else { - _removeSlashMenu(); - } - } - - void _onBlockTap(int index) { - final provider = context.read(); - provider.setFocusedBlock(index); - - final block = provider.blocks[index]; - if (block.type == NoteBlockType.divider) return; - - final node = _getFocusNode(block, index); - if (!node.hasFocus) { - node.requestFocus(); - } - } - - void _onToggleChecklist(int index) { - context.read().toggleChecklist(index); - } - - /// 处理键盘事件 - KeyEventResult _handleKeyEvent(int index, KeyEvent event) { - if (event is! KeyDownEvent) return KeyEventResult.ignored; - - final provider = context.read(); - final block = provider.blocks[index]; - final controller = _controllers[block.id]; - if (controller == null) return KeyEventResult.ignored; - - final sel = controller.selection; - - // Enter → 分割 block - if (event.logicalKey == LogicalKeyboardKey.enter && - !HardwareKeyboard.instance.isShiftPressed) { - final pos = sel.isValid ? sel.baseOffset : controller.text.length; - provider.splitBlock(index, pos); - - // 聚焦新 block - WidgetsBinding.instance.addPostFrameCallback((_) { - _focusBlockAtIndex(index + 1, atStart: true); - }); - return KeyEventResult.handled; - } - - // Backspace 在位置0 → 合并到前一个 - if (event.logicalKey == LogicalKeyboardKey.backspace && - sel.isValid && - sel.isCollapsed && - sel.baseOffset == 0 && - index > 0) { - final cursorPos = provider.mergeWithPrevious(index); - WidgetsBinding.instance.addPostFrameCallback((_) { - _focusBlockAtIndex(index - 1, atCursor: cursorPos); - }); - return KeyEventResult.handled; - } - - // ↑ 在位置0 → 聚焦上一个 - if (event.logicalKey == LogicalKeyboardKey.arrowUp && - sel.isValid && - sel.isCollapsed && - sel.baseOffset == 0 && - index > 0) { - _focusBlockAtIndex(index - 1, atEnd: true); - return KeyEventResult.handled; - } - - // ↓ 在末位 → 聚焦下一个 - if (event.logicalKey == LogicalKeyboardKey.arrowDown && - sel.isValid && - sel.isCollapsed && - sel.baseOffset == controller.text.length && - index < provider.blocks.length - 1) { - _focusBlockAtIndex(index + 1, atStart: true); - return KeyEventResult.handled; - } - - return KeyEventResult.ignored; - } - - void _focusBlockAtIndex(int index, {bool atStart = false, bool atEnd = false, int? atCursor}) { - final provider = context.read(); - if (index < 0 || index >= provider.blocks.length) return; - - final block = provider.blocks[index]; - if (block.type == NoteBlockType.divider) return; - - provider.setFocusedBlock(index); - final node = _getFocusNode(block, index); - final controller = _getController(block); - - // 等 controller 同步后设置光标 - WidgetsBinding.instance.addPostFrameCallback((_) { - if (!node.hasFocus) node.requestFocus(); - int cursorPos; - if (atCursor != null) { - cursorPos = atCursor; - } else if (atEnd) { - cursorPos = controller.text.length; - } else { - cursorPos = 0; - } - controller.selection = TextSelection.collapsed(offset: cursorPos); - }); - } - - // ========== Slash Menu ========== - - void _showSlashMenu(int blockIndex, String text) { - _removeSlashMenu(); - - final query = text.length > 1 ? text.substring(1) : ''; - - _slashMenuOverlay = OverlayEntry( - builder: (context) => Positioned( - bottom: MediaQuery.of(context).viewInsets.bottom + 60, - left: 16, - right: 16, - child: Material( - elevation: 8, - borderRadius: BorderRadius.circular(12), - child: SlashCommandMenu( - query: query, - onSelect: (type) { - _removeSlashMenu(); - final provider = context.read(); - // 清除 / 文本 - provider.updateBlockSilent( - blockIndex, - provider.blocks[blockIndex].copyWith(text: ''), - ); - _controllers[provider.blocks[blockIndex].id]?.text = ''; - // 转换类型 - provider.convertBlockType(blockIndex, type); - }, - onDismiss: _removeSlashMenu, - ), - ), - ), - ); - Overlay.of(context).insert(_slashMenuOverlay!); - } - - void _removeSlashMenu() { - _slashMenuOverlay?.remove(); - _slashMenuOverlay = null; - } - - // ========== Build ========== - - @override - Widget build(BuildContext context) { - return Consumer( - builder: (context, provider, _) { - final blocks = provider.blocks; - - // 清理已删除 block 对应的控制器和焦点节点,防止内存泄漏 - _cleanupStaleEntries(blocks); - - return ListView.builder( - controller: _scrollController, - itemCount: blocks.length, - itemBuilder: (context, index) { - final block = blocks[index]; - final isFocused = provider.focusedBlockIndex == index; - - // 计算有序列表序号 - int numberedIndex = 1; - if (block.type == NoteBlockType.numberedList) { - for (int i = index - 1; i >= 0; i--) { - if (blocks[i].type == NoteBlockType.numberedList) { - numberedIndex++; - } else { - break; - } - } - } - - // 同步 controller 文本 - final controller = isFocused ? _getController(block) : null; - if (controller != null && controller.text != block.text) { - // 用 addPostFrameCallback 避免 build 期间修改 - WidgetsBinding.instance.addPostFrameCallback((_) { - if (controller.text != block.text) { - controller.text = block.text; - } - }); - } - - return Padding( - key: ValueKey(block.id), - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Block 内容 - Expanded( - child: NotePlusBlockWidget( - block: block, - index: index, - numberedIndex: numberedIndex, - isFocused: isFocused, - controller: controller, - focusNode: isFocused ? _getFocusNode(block, index) : null, - onTap: () => _onBlockTap(index), - onToggleChecklist: block.type == NoteBlockType.checklist - ? () => _onToggleChecklist(index) - : null, - ), - ), - ], - ), - ); - }, - ); - }, - ); - } -} diff --git a/lib/widgets/note_plus/slash_command_menu.dart b/lib/widgets/note_plus/slash_command_menu.dart deleted file mode 100644 index bd5d1bc..0000000 --- a/lib/widgets/note_plus/slash_command_menu.dart +++ /dev/null @@ -1,127 +0,0 @@ -import 'package:flutter/material.dart'; -import '../../models/note_plus_models.dart'; - -/// 斜杠命令菜单 -/// -/// 输入 `/` 后弹出,显示可插入的 block 类型列表。 -/// 支持按中文关键词过滤。 -class SlashCommandMenu extends StatefulWidget { - final String query; - final void Function(NoteBlockType) onSelect; - final VoidCallback onDismiss; - - const SlashCommandMenu({ - super.key, - required this.query, - required this.onSelect, - required this.onDismiss, - }); - - @override - State createState() => _SlashCommandMenuState(); -} - -class _SlashCommandMenuState extends State { - int _selectedIndex = 0; - - static const _menuItems = [ - _MenuItem(NoteBlockType.paragraph, Icons.notes, '正文', '段落'), - _MenuItem(NoteBlockType.heading1, Icons.title, '标题1', '大标题'), - _MenuItem(NoteBlockType.heading2, Icons.title, '标题2', '中标题'), - _MenuItem(NoteBlockType.heading3, Icons.title, '标题3', '小标题'), - _MenuItem(NoteBlockType.bulletList, Icons.format_list_bulleted, '无序列表', '圆点列表'), - _MenuItem(NoteBlockType.numberedList, Icons.format_list_numbered, '有序列表', '数字列表'), - _MenuItem(NoteBlockType.checklist, Icons.check_box_outlined, '待办', '清单'), - _MenuItem(NoteBlockType.quote, Icons.format_quote, '引用', '引述'), - _MenuItem(NoteBlockType.codeBlock, Icons.code, '代码块', '源码'), - _MenuItem(NoteBlockType.divider, Icons.horizontal_rule, '分割线', '分隔'), - ]; - - List<_MenuItem> get _filtered { - if (widget.query.isEmpty) return _menuItems; - final q = widget.query.toLowerCase(); - return _menuItems.where((item) { - return item.label.toLowerCase().contains(q) || - item.keywords.toLowerCase().contains(q); - }).toList(); - } - - @override - void didUpdateWidget(SlashCommandMenu oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.query != widget.query) { - final items = _filtered; - if (_selectedIndex >= items.length) { - _selectedIndex = items.isEmpty ? 0 : items.length - 1; - } - } - } - - @override - Widget build(BuildContext context) { - final colors = Theme.of(context).colorScheme; - final items = _filtered; - - if (items.isEmpty) { - return Container( - padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20), - child: Text('无匹配项', - style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.4))), - ); - } - - return ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 320), - child: ListView.builder( - shrinkWrap: true, - padding: const EdgeInsets.symmetric(vertical: 8), - itemCount: items.length, - itemBuilder: (context, index) { - final item = items[index]; - final isSelected = index == _selectedIndex; - - return InkWell( - onTap: () => widget.onSelect(item.type), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), - color: isSelected - ? colors.primary.withValues(alpha: 0.08) - : null, - child: Row( - children: [ - Icon(item.icon, size: 20, - color: isSelected - ? colors.primary - : colors.onSurface.withValues(alpha: 0.5)), - const SizedBox(width: 12), - Expanded( - child: Text(item.label, - style: TextStyle( - fontSize: 14, - fontWeight: isSelected ? FontWeight.w500 : FontWeight.w400, - color: isSelected ? colors.primary : colors.onSurface, - )), - ), - Text(item.keywords, - style: TextStyle( - fontSize: 11, - color: colors.onSurface.withValues(alpha: 0.3), - )), - ], - ), - ), - ); - }, - ), - ); - } -} - -class _MenuItem { - final NoteBlockType type; - final IconData icon; - final String label; - final String keywords; - - const _MenuItem(this.type, this.icon, this.label, this.keywords); -} diff --git a/lib/widgets/note_plus_list_item.dart b/lib/widgets/note_plus_list_item.dart deleted file mode 100644 index 94f045e..0000000 --- a/lib/widgets/note_plus_list_item.dart +++ /dev/null @@ -1,161 +0,0 @@ -import 'package:flutter/material.dart'; -import '../../models/note_plus_models.dart'; - -/// AppFlowy 风格的文件树列表项 -/// -/// 匹配 AppFlowy ViewSectionItem 的样式: -/// - 固定高度 26px -/// - 16px 图标 + 文字 -/// - 12px 字号 -/// - hover 时显示操作按钮 -class NotePlusTreeItem extends StatefulWidget { - final NotePlusDocument doc; - final bool isSelected; - final VoidCallback onTap; - final VoidCallback? onRename; - final VoidCallback? onDelete; - - const NotePlusTreeItem({ - super.key, - required this.doc, - required this.isSelected, - required this.onTap, - this.onRename, - this.onDelete, - }); - - @override - State createState() => _NotePlusTreeItemState(); -} - -class _NotePlusTreeItemState extends State { - bool _isHovering = false; - - @override - Widget build(BuildContext context) { - final colors = Theme.of(context).colorScheme; - final isSelected = widget.isSelected; - final showActions = _isHovering || isSelected; - - return MouseRegion( - onEnter: (_) => setState(() => _isHovering = true), - onExit: (_) => setState(() => _isHovering = false), - child: GestureDetector( - onTap: widget.onTap, - child: Container( - height: 26, - padding: const EdgeInsets.only(left: 22, right: 6), - decoration: BoxDecoration( - color: isSelected - ? colors.primary.withValues(alpha: 0.08) - : _isHovering - ? colors.onSurface.withValues(alpha: 0.04) - : null, - borderRadius: BorderRadius.circular(4), - ), - child: Row( - children: [ - // 文档图标 - SizedBox( - width: 16, - height: 16, - child: Icon( - _getBlockIcon(widget.doc), - size: 14, - color: isSelected - ? colors.primary - : colors.onSurface.withValues(alpha: 0.4), - ), - ), - const SizedBox(width: 2), - // 文档标题 - Expanded( - child: Text( - widget.doc.title.isEmpty ? '无标题' : widget.doc.title, - style: TextStyle( - fontSize: 12, - fontWeight: isSelected ? FontWeight.w500 : FontWeight.w400, - color: isSelected - ? colors.primary - : colors.onSurface.withValues(alpha: 0.75), - ), - overflow: TextOverflow.clip, - maxLines: 1, - ), - ), - // hover 操作按钮 - if (showActions) - _buildActions(colors), - ], - ), - ), - ), - ); - } - - Widget _buildActions(ColorScheme colors) { - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (widget.onRename != null) - _ActionButton( - icon: Icons.edit_outlined, - onTap: widget.onRename!, - colors: colors, - ), - if (widget.onDelete != null) - _ActionButton( - icon: Icons.delete_outline, - onTap: widget.onDelete!, - colors: colors, - ), - ], - ); - } - - IconData _getBlockIcon(NotePlusDocument doc) { - // 根据第一个块的类型显示图标 - if (doc.blocks.isEmpty) return Icons.description_outlined; - final firstType = doc.blocks.first.type; - switch (firstType) { - case NoteBlockType.heading1: - case NoteBlockType.heading2: - case NoteBlockType.heading3: - return Icons.title; - case NoteBlockType.checklist: - return Icons.check_box_outlined; - case NoteBlockType.codeBlock: - return Icons.code; - case NoteBlockType.quote: - return Icons.format_quote; - default: - return Icons.description_outlined; - } - } -} - -class _ActionButton extends StatelessWidget { - final IconData icon; - final VoidCallback onTap; - final ColorScheme colors; - - const _ActionButton({ - required this.icon, - required this.onTap, - required this.colors, - }); - - @override - Widget build(BuildContext context) { - return SizedBox( - width: 20, - height: 20, - child: InkWell( - onTap: onTap, - borderRadius: BorderRadius.circular(4), - child: Icon(icon, size: 14, - color: colors.onSurface.withValues(alpha: 0.4)), - ), - ); - } -} diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 9c2de87..7fb92d3 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -10,12 +10,10 @@ import file_picker import file_selector_macos import flutter_inappwebview_macos import package_info_plus -import quill_native_bridge_macos import share_plus import shared_preferences_foundation import sqflite_darwin import url_launcher_macos -import webview_flutter_wkwebview func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin")) @@ -23,10 +21,8 @@ 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")) - QuillNativeBridgePlugin.register(with: registry.registrar(forPlugin: "QuillNativeBridgePlugin")) 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")) - WebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "WebViewFlutterPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 754b9bc..7b373ba 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -41,14 +41,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" - charcode: - dependency: transitive - description: - name: charcode - sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a - url: "https://pub.dev" - source: hosted - version: "1.4.0" checked_yaml: dependency: transitive description: @@ -105,38 +97,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.7" - csslib: - dependency: transitive - description: - name: csslib - sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" - url: "https://pub.dev" - source: hosted - version: "1.0.2" - cupertino_icons: - dependency: "direct main" - description: - name: cupertino_icons - sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd" - url: "https://pub.dev" - source: hosted - version: "1.0.9" - dart_quill_delta: - dependency: transitive - description: - name: dart_quill_delta - sha256: bddb0b2948bd5b5a328f1651764486d162c59a8ccffd4c63e8b2c5e44be1dac4 - url: "https://pub.dev" - source: hosted - version: "10.8.3" - diff_match_patch: - dependency: transitive - description: - name: diff_match_patch - sha256: "2efc9e6e8f449d0abe15be240e2c2a3bcd977c8d126cfd70598aee60af35c0a4" - url: "https://pub.dev" - source: hosted - version: "0.4.1" dynamic_color: dependency: "direct main" description: @@ -161,22 +121,6 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.1" - extended_text_field: - dependency: "direct main" - description: - name: extended_text_field - sha256: "3996195c117c6beb734026a7bc0ba80d7e4e84e4edd4728caa544d8209ab4d7d" - url: "https://pub.dev" - source: hosted - version: "16.0.2" - extended_text_library: - dependency: transitive - description: - name: extended_text_library - sha256: "13d99f8a10ead472d5e2cf4770d3d047203fe5054b152e9eb5dc692a71befbba" - url: "https://pub.dev" - source: hosted - version: "12.0.1" fake_async: dependency: transitive description: @@ -262,14 +206,6 @@ packages: description: flutter source: sdk version: "0.0.0" - flutter_colorpicker: - dependency: transitive - description: - name: flutter_colorpicker - sha256: "969de5f6f9e2a570ac660fb7b501551451ea2a1ab9e2097e89475f60e07816ea" - url: "https://pub.dev" - source: hosted - version: "1.1.0" flutter_inappwebview: dependency: "direct main" description: @@ -334,46 +270,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.0" - flutter_keyboard_visibility_linux: - dependency: transitive - description: - name: flutter_keyboard_visibility_linux - sha256: "6fba7cd9bb033b6ddd8c2beb4c99ad02d728f1e6e6d9b9446667398b2ac39f08" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - flutter_keyboard_visibility_macos: - dependency: transitive - description: - name: flutter_keyboard_visibility_macos - sha256: c5c49b16fff453dfdafdc16f26bdd8fb8d55812a1d50b0ce25fc8d9f2e53d086 - url: "https://pub.dev" - source: hosted - version: "1.0.0" - flutter_keyboard_visibility_platform_interface: - dependency: transitive - description: - name: flutter_keyboard_visibility_platform_interface - sha256: e43a89845873f7be10cb3884345ceb9aebf00a659f479d1c8f4293fcb37022a4 - url: "https://pub.dev" - source: hosted - version: "2.0.0" - flutter_keyboard_visibility_temp_fork: - dependency: transitive - description: - name: flutter_keyboard_visibility_temp_fork - sha256: e3d02900640fbc1129245540db16944a0898b8be81694f4bf04b6c985bed9048 - url: "https://pub.dev" - source: hosted - version: "0.1.5" - flutter_keyboard_visibility_windows: - dependency: transitive - description: - name: flutter_keyboard_visibility_windows - sha256: fc4b0f0b6be9b93ae527f3d527fb56ee2d918cd88bbca438c478af7bcfd0ef73 - url: "https://pub.dev" - source: hosted - version: "1.0.0" flutter_launcher_icons: dependency: "direct dev" description: @@ -411,22 +307,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.35" - flutter_quill: - dependency: "direct main" - description: - name: flutter_quill - sha256: b96bb8525afdeaaea52f5d02f525e05cc34acd176467ab6d6f35d434cf14fde2 - url: "https://pub.dev" - source: hosted - version: "11.5.0" - flutter_quill_delta_from_html: - dependency: transitive - description: - name: flutter_quill_delta_from_html - sha256: "0eb801ea8dd498cadc057507af5da794d4c9599ce58b2569cb3d4bb53ba8bed2" - url: "https://pub.dev" - source: hosted - version: "1.5.3" flutter_staggered_grid_view: dependency: "direct main" description: @@ -461,14 +341,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.2" - html: - dependency: transitive - description: - name: html - sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" - url: "https://pub.dev" - source: hosted - version: "0.15.6" http: dependency: "direct main" description: @@ -861,70 +733,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" - quill_native_bridge: - dependency: transitive - description: - name: quill_native_bridge - sha256: "76a16512e398e84216f3f659f7cb18a89ec1e141ea908e954652b4ce6cf15b18" - url: "https://pub.dev" - source: hosted - version: "11.1.0" - quill_native_bridge_android: - dependency: transitive - description: - name: quill_native_bridge_android - sha256: b75c7e6ede362a7007f545118e756b1f19053994144ec9eda932ce5e54a57569 - url: "https://pub.dev" - source: hosted - version: "0.0.1+2" - quill_native_bridge_ios: - dependency: transitive - description: - name: quill_native_bridge_ios - sha256: d23de3cd7724d482fe2b514617f8eedc8f296e120fb297368917ac3b59d8099f - url: "https://pub.dev" - source: hosted - version: "0.0.1" - quill_native_bridge_macos: - dependency: transitive - description: - name: quill_native_bridge_macos - sha256: "1c0631bd1e2eee765a8b06017c5286a4e829778f4585736e048eb67c97af8a77" - url: "https://pub.dev" - source: hosted - version: "0.0.1" - quill_native_bridge_platform_interface: - dependency: transitive - description: - name: quill_native_bridge_platform_interface - sha256: "8264a2bdb8a294c31377a27b46c0f8717fa9f968cf113f7dc52d332ed9c84526" - url: "https://pub.dev" - source: hosted - version: "0.0.2+1" - quill_native_bridge_web: - dependency: transitive - description: - name: quill_native_bridge_web - sha256: "7c723f6824b0250d7f33e8b6c23f2f8eb0103fe48ee7ebf47ab6786b64d5c05d" - url: "https://pub.dev" - source: hosted - version: "0.0.2" - quill_native_bridge_windows: - dependency: transitive - description: - name: quill_native_bridge_windows - sha256: "3f96ced19e3206ddf4f6f7dde3eb16bdd05e10294964009ea3a806d995aa7caa" - url: "https://pub.dev" - source: hosted - version: "0.0.2" - quiver: - dependency: transitive - description: - name: quiver - sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2 - url: "https://pub.dev" - source: hosted - version: "3.2.2" record_use: dependency: transitive description: @@ -1070,10 +878,10 @@ packages: dependency: transitive description: name: sqlite3 - sha256: "37356bcb56ce0d9404d602c41e4bdb7765e7e9732a3e47adb3d98c556a6abdad" + sha256: "752d9d746052359a2022f588bb979f2e7c4e0f9e4b6a1c3121f7626a1574974b" url: "https://pub.dev" source: hosted - version: "3.3.3" + version: "3.3.4" stack_trace: dependency: transitive description: @@ -1226,38 +1034,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" - webview_flutter: - dependency: "direct main" - description: - name: webview_flutter - sha256: e4d3474277c5043f5f3100ed27d94ad693abfd5c602b0221c719a4497e4ba1e4 - url: "https://pub.dev" - source: hosted - version: "4.14.0" - webview_flutter_android: - dependency: transitive - description: - name: webview_flutter_android - sha256: ad5182eff9a550925330cb9f0cb038eddfdd5712aba8b77aa0f0400e50f6e688 - url: "https://pub.dev" - source: hosted - version: "4.12.0" - webview_flutter_platform_interface: - dependency: transitive - description: - name: webview_flutter_platform_interface - sha256: "1221c1b12f5278791042f2ec2841743784cf25c5a644e23d6680e5d718824f04" - url: "https://pub.dev" - source: hosted - version: "2.15.1" - webview_flutter_wkwebview: - dependency: transitive - description: - name: webview_flutter_wkwebview - sha256: "82648217f537573e1ca9ae9952d3eacedca6ab5aee69dc84445fc763766dcea2" - url: "https://pub.dev" - source: hosted - version: "3.25.1" win32: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index fb4c4c2..159e54f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,11 +11,9 @@ dependencies: sdk: flutter flutter_localizations: sdk: flutter - cupertino_icons: ^1.0.8 provider: ^6.1.2 fl_chart: ^0.69.0 sqflite: ^2.3.0 - sqflite_common_ffi: ^2.3.0 path: ^1.8.3 image_picker: ^1.0.4 path_provider: ^2.1.1 @@ -30,14 +28,12 @@ dependencies: flutter_staggered_grid_view: ^0.7.0 http: ^1.2.0 url_launcher: ^6.2.5 - webview_flutter: ^4.8.0 flutter_inappwebview: ^6.1.5 dynamic_color: ^1.8.1 package_info_plus: ^8.0.0 - extended_text_field: ^16.0.2 uuid: ^4.5.0 expandable: ^5.0.1 - flutter_quill: ^11.5.0 + sqflite_common_ffi: ^2.3.0 dev_dependencies: flutter_test: @@ -48,11 +44,16 @@ dev_dependencies: flutter_launcher_icons: android: "ic_launcher" ios: false - image_path: "assets/icon/app_icon.png" + image_path: "assets/icon/app_icon.webp" adaptive_icon_background: "#FFFFFF" - adaptive_icon_foreground: "assets/icon/app_icon.png" + adaptive_icon_foreground: "assets/icon/app_icon.webp" min_sdk_android: 21 +hooks: + user_defines: + sqlite3: + source: system + flutter: uses-material-design: true