generated from dellevin/template
笔记编辑功能修复
This commit is contained in:
@@ -30,13 +30,18 @@ List<String> parseStringListGeneric(dynamic data) {
|
||||
return data.map((e) => e.toString()).toList();
|
||||
}
|
||||
if (data is String) {
|
||||
if (data.isEmpty) return [];
|
||||
try {
|
||||
final decoded = jsonDecode(data);
|
||||
if (decoded is List) {
|
||||
return decoded.map((e) => e.toString()).toList();
|
||||
}
|
||||
// JSON 解析成功但不是 List(如 Map、String),保留原始值
|
||||
return [data];
|
||||
} catch (e) {
|
||||
return data.split(',').map((s) => s.trim()).where((s) => s.isNotEmpty).toList();
|
||||
// JSON 解析失败,按逗号分割;若无逗号则作为单元素保留
|
||||
final split = data.split(',').map((s) => s.trim()).where((s) => s.isNotEmpty).toList();
|
||||
return split.isNotEmpty ? split : [data];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
|
||||
@@ -412,6 +412,7 @@ class _CloudSheetContentState extends State<_CloudSheetContent> {
|
||||
}
|
||||
|
||||
Future<void> _loadRemoteInfo() async {
|
||||
if (!mounted) return;
|
||||
setState(() => _loading = true);
|
||||
final info = await WebDAVService.instance.getRemoteBackupInfo();
|
||||
if (mounted) {
|
||||
|
||||
@@ -2024,9 +2024,9 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
|
||||
Navigator.of(context).pop(); // close dialog
|
||||
provider.selectMovie(null);
|
||||
} else {
|
||||
final navigator = Navigator.of(context);
|
||||
navigator.pop();
|
||||
navigator.pop();
|
||||
// 先关 dialog,再关详情页;分两步避免连续 pop 导致的导航栈异常
|
||||
Navigator.of(context).pop(); // close dialog
|
||||
if (mounted) Navigator.of(context).pop(); // close detail page
|
||||
}
|
||||
if (mounted && context.mounted) {
|
||||
ToastUtil.show(context, '已删除');
|
||||
|
||||
@@ -10,6 +10,7 @@ import '../../models/data_models.dart';
|
||||
import '../../utils/toast_util.dart';
|
||||
import '../../utils/image_path_helper.dart';
|
||||
import '../../widgets/fade_in_local_image.dart';
|
||||
import '../../widgets/tag_side_panel.dart';
|
||||
import '../../widgets/vditor_editor.dart';
|
||||
|
||||
class NoteAddPage extends StatefulWidget {
|
||||
@@ -126,7 +127,6 @@ class _NoteAddPageState extends State<NoteAddPage> {
|
||||
// 标题输入
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
decoration: BoxDecoration(border: Border(bottom: BorderSide(color: colors.outlineVariant, width: 0.5))),
|
||||
child: Center(child: ConstrainedBox(constraints: const BoxConstraints(maxWidth: 720),
|
||||
child: TextField(
|
||||
controller: _titleCtrl,
|
||||
@@ -143,6 +143,13 @@ class _NoteAddPageState extends State<NoteAddPage> {
|
||||
),
|
||||
)),
|
||||
),
|
||||
// 标签行 — 标题下面,靠左
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
|
||||
child: Center(child: ConstrainedBox(constraints: const BoxConstraints(maxWidth: 720),
|
||||
child: Align(alignment: Alignment.centerLeft, child: _buildTagRow(colors)),
|
||||
)),
|
||||
),
|
||||
// 内容编辑
|
||||
Expanded(
|
||||
child: VditorEditor(
|
||||
@@ -169,6 +176,64 @@ class _NoteAddPageState extends State<NoteAddPage> {
|
||||
]);
|
||||
}
|
||||
|
||||
Widget _buildTagRow(ColorScheme colors) {
|
||||
return Wrap(
|
||||
spacing: 6,
|
||||
runSpacing: 4,
|
||||
children: [
|
||||
for (final tag in _tags)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
Text(tag, style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.6))),
|
||||
const SizedBox(width: 3),
|
||||
GestureDetector(
|
||||
onTap: () => setState(() => _tags.remove(tag)),
|
||||
child: Icon(Icons.close, size: 10, color: colors.onSurface.withValues(alpha: 0.3)),
|
||||
),
|
||||
]),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: _showTagPanel,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(color: colors.onSurface.withValues(alpha: 0.25), width: 1),
|
||||
),
|
||||
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
Icon(Icons.add, size: 12, color: colors.onSurface.withValues(alpha: 0.35)),
|
||||
const SizedBox(width: 2),
|
||||
Text('标签', style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.35))),
|
||||
]),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showTagPanel() async {
|
||||
final provider = context.read<AppProvider>();
|
||||
final tagRows = await provider.getTags('note_tag');
|
||||
final allTags = tagRows.map((t) => t['name'] as String).toSet();
|
||||
for (final note in provider.notes) {
|
||||
allTags.addAll(note.tags);
|
||||
}
|
||||
if (!mounted) return;
|
||||
TagSidePanel.show(
|
||||
context: context,
|
||||
selectedTags: List.from(_tags),
|
||||
allAvailableTags: allTags.toList()..sort(),
|
||||
onTagsChanged: (newTags) {
|
||||
setState(() => _tags = newTags);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPreviewArea(ColorScheme colors) {
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
@@ -178,6 +243,21 @@ class _NoteAddPageState extends State<NoteAddPage> {
|
||||
style: TextStyle(fontSize: 24, fontWeight: FontWeight.w600, color: colors.onSurface, height: 1.3)),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
// 标签
|
||||
if (_tags.isNotEmpty) ...[
|
||||
Wrap(spacing: 6, runSpacing: 4, children: [
|
||||
for (final tag in _tags)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||
decoration: BoxDecoration(
|
||||
color: colors.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(tag, style: TextStyle(fontSize: 12, color: colors.onPrimaryContainer, fontWeight: FontWeight.w500)),
|
||||
),
|
||||
]),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
Markdown(
|
||||
data: _contentCtrl.text,
|
||||
styleSheet: _buildMarkdownStyleSheet(colors),
|
||||
|
||||
@@ -12,6 +12,7 @@ import '../../utils/toast_util.dart';
|
||||
import '../../utils/image_path_helper.dart';
|
||||
import '../../utils/responsive.dart';
|
||||
import '../../widgets/vditor_editor.dart';
|
||||
import '../../widgets/tag_side_panel.dart';
|
||||
import 'note_share_page.dart';
|
||||
|
||||
/// 笔记详情页
|
||||
@@ -444,7 +445,6 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
|
||||
// 标题输入
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
decoration: BoxDecoration(border: Border(bottom: BorderSide(color: colors.outlineVariant, width: 0.5))),
|
||||
child: Center(child: ConstrainedBox(constraints: const BoxConstraints(maxWidth: 720),
|
||||
child: TextField(controller: _titleCtrl, maxLines: 1,
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: colors.onSurface, height: 1.4),
|
||||
@@ -454,6 +454,13 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
|
||||
onChanged: (_) => setState(() {})),
|
||||
)),
|
||||
),
|
||||
// 标签行 — 标题下面,靠左
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
|
||||
child: Center(child: ConstrainedBox(constraints: const BoxConstraints(maxWidth: 720),
|
||||
child: Align(alignment: Alignment.centerLeft, child: _buildEditTagRow(colors)),
|
||||
)),
|
||||
),
|
||||
// 内容编辑
|
||||
Expanded(
|
||||
child: VditorEditor(
|
||||
@@ -480,6 +487,64 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
|
||||
]);
|
||||
}
|
||||
|
||||
Widget _buildEditTagRow(ColorScheme colors) {
|
||||
return Wrap(
|
||||
spacing: 6,
|
||||
runSpacing: 4,
|
||||
children: [
|
||||
for (final tag in _editTags)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
Text(tag, style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.6))),
|
||||
const SizedBox(width: 3),
|
||||
GestureDetector(
|
||||
onTap: () => setState(() => _editTags.remove(tag)),
|
||||
child: Icon(Icons.close, size: 10, color: colors.onSurface.withValues(alpha: 0.3)),
|
||||
),
|
||||
]),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: _showEditTagPanel,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(color: colors.onSurface.withValues(alpha: 0.25), width: 1),
|
||||
),
|
||||
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
Icon(Icons.add, size: 12, color: colors.onSurface.withValues(alpha: 0.35)),
|
||||
const SizedBox(width: 2),
|
||||
Text('标签', style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.35))),
|
||||
]),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showEditTagPanel() async {
|
||||
final provider = context.read<AppProvider>();
|
||||
final tagRows = await provider.getTags('note_tag');
|
||||
final allTags = tagRows.map((t) => t['name'] as String).toSet();
|
||||
for (final note in provider.notes) {
|
||||
allTags.addAll(note.tags);
|
||||
}
|
||||
if (!mounted) return;
|
||||
TagSidePanel.show(
|
||||
context: context,
|
||||
selectedTags: List.from(_editTags),
|
||||
allAvailableTags: allTags.toList()..sort(),
|
||||
onTagsChanged: (newTags) {
|
||||
setState(() => _editTags = newTags);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPreviewArea(ColorScheme colors, Note note) {
|
||||
return ListView(
|
||||
padding: const EdgeInsets.symmetric(vertical: 24),
|
||||
|
||||
@@ -143,6 +143,12 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final topPadding = MediaQuery.of(context).padding.top;
|
||||
|
||||
// Windows 桌面端:与编辑页一致的布局
|
||||
if (Platform.isWindows) {
|
||||
return _buildWindowsDesktopLayout(colors, topPadding);
|
||||
}
|
||||
|
||||
return PopScope(
|
||||
canPop: false,
|
||||
onPopInvokedWithResult: (didPop, result) async {
|
||||
@@ -156,7 +162,6 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
body: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final keyboardH = MediaQuery.of(context).viewInsets.bottom;
|
||||
final contentH = constraints.maxHeight * 0.4;
|
||||
return Stack(
|
||||
children: [
|
||||
Column(
|
||||
@@ -164,19 +169,16 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
// 顶部区域 — 固定不动
|
||||
_buildHeader(colors, topPadding),
|
||||
|
||||
// 可滚动内容
|
||||
// 移动端:CustomScrollView 包裹所有内容
|
||||
Expanded(
|
||||
child: CustomScrollView(
|
||||
controller: _scrollController,
|
||||
physics: _editorTouched ? const NeverScrollableScrollPhysics() : null,
|
||||
slivers: [
|
||||
// 标题行(点击编辑)
|
||||
SliverToBoxAdapter(child: _buildTitleInput(colors)),
|
||||
|
||||
// 编辑区域 — 高度随内容撑开,触摸时禁用外层滚动
|
||||
SliverToBoxAdapter(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(minHeight: contentH),
|
||||
constraints: BoxConstraints(minHeight: constraints.maxHeight * 0.4),
|
||||
child: GestureDetector(
|
||||
onTapDown: (_) => setState(() => _editorTouched = true),
|
||||
onTapUp: (_) => setState(() => _editorTouched = false),
|
||||
@@ -184,8 +186,6 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
child: _buildEditor(),
|
||||
)),
|
||||
),
|
||||
|
||||
// 图片 + 标签 + 字数 — 随内容撑开
|
||||
SliverToBoxAdapter(child: _buildImageGrid()),
|
||||
SliverToBoxAdapter(child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
|
||||
@@ -198,8 +198,6 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.3)),
|
||||
),
|
||||
)),
|
||||
|
||||
// 与工具栏的间距
|
||||
const SliverToBoxAdapter(child: SizedBox(height: 80)),
|
||||
],
|
||||
),
|
||||
@@ -222,6 +220,128 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
);
|
||||
}
|
||||
|
||||
/// Windows 桌面端布局 — 与编辑页一致
|
||||
Widget _buildWindowsDesktopLayout(ColorScheme colors, double topPadding) {
|
||||
return PopScope(
|
||||
canPop: false,
|
||||
onPopInvokedWithResult: (didPop, result) async {
|
||||
if (didPop) return;
|
||||
final shouldPop = await _confirmLeave();
|
||||
if (shouldPop && context.mounted) Navigator.pop(context);
|
||||
},
|
||||
child: Scaffold(
|
||||
backgroundColor: colors.surface,
|
||||
body: Column(
|
||||
children: [
|
||||
// 顶栏 — 与编辑页一致
|
||||
Container(
|
||||
height: 52,
|
||||
decoration: BoxDecoration(color: colors.surface,
|
||||
border: Border(bottom: BorderSide(color: colors.outlineVariant, width: 0.5))),
|
||||
child: Row(children: [
|
||||
const SizedBox(width: 8),
|
||||
IconButton(icon: Icon(Icons.close, color: colors.onSurface, size: 18),
|
||||
onPressed: () async {
|
||||
final shouldPop = await _confirmLeave();
|
||||
if (shouldPop && context.mounted) Navigator.pop(context);
|
||||
}),
|
||||
Expanded(child: Text(_titleController.text.isNotEmpty ? _titleController.text : '新建笔记',
|
||||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: colors.onSurface.withValues(alpha: 0.6)),
|
||||
maxLines: 1, overflow: TextOverflow.ellipsis)),
|
||||
if (_saveStatus == 'saved')
|
||||
Padding(padding: const EdgeInsets.only(right: 8),
|
||||
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
Container(width: 6, height: 6, decoration: const BoxDecoration(color: Colors.green, shape: BoxShape.circle)),
|
||||
const SizedBox(width: 4),
|
||||
Text('已保存', style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||
])),
|
||||
FilledButton.icon(onPressed: _saveNote,
|
||||
icon: const Icon(Icons.check, size: 16), label: const Text('保存'),
|
||||
style: FilledButton.styleFrom(padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)))),
|
||||
const SizedBox(width: 16),
|
||||
]),
|
||||
),
|
||||
// 标题输入 — 与编辑页一致
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Center(child: ConstrainedBox(constraints: const BoxConstraints(maxWidth: 720),
|
||||
child: TextField(controller: _titleController, maxLines: 1,
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: colors.onSurface, height: 1.4),
|
||||
decoration: InputDecoration(hintText: '添加标题',
|
||||
hintStyle: TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: colors.onSurface.withValues(alpha: 0.2), height: 1.4),
|
||||
border: InputBorder.none, enabledBorder: InputBorder.none, focusedBorder: InputBorder.none, isDense: true, contentPadding: EdgeInsets.zero),
|
||||
onChanged: (_) => setState(() {})),
|
||||
)),
|
||||
),
|
||||
// 标签行 — 与编辑页一致
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
|
||||
child: Center(child: ConstrainedBox(constraints: const BoxConstraints(maxWidth: 720),
|
||||
child: Align(alignment: Alignment.centerLeft, child: _buildTagChips()),
|
||||
)),
|
||||
),
|
||||
// 内容编辑
|
||||
Expanded(
|
||||
child: VditorEditor(
|
||||
key: _vditorKey,
|
||||
initialContent: _contentController.text,
|
||||
noteId: _isEditing && widget.note != null ? widget.note!.id : (_tempNoteId ?? ''),
|
||||
isDark: Theme.of(context).brightness == Brightness.dark,
|
||||
surfaceColor: colors.surface,
|
||||
onContentChanged: (value) {
|
||||
_contentController.text = value;
|
||||
_onTextChanged();
|
||||
},
|
||||
),
|
||||
),
|
||||
// 图片网格
|
||||
if (_images.isNotEmpty) _buildEditImageGrid(colors),
|
||||
// 底部字数
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Row(mainAxisAlignment: MainAxisAlignment.end, children: [
|
||||
Text('${_contentController.text.length} 字', style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.3))),
|
||||
]),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Windows 桌面端图片网格 — 与编辑页一致
|
||||
Widget _buildEditImageGrid(ColorScheme colors) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.fromLTRB(16, 6, 16, 0),
|
||||
height: 72,
|
||||
child: ListView.separated(scrollDirection: Axis.horizontal,
|
||||
itemCount: _images.length + 1,
|
||||
separatorBuilder: (_, __) => const SizedBox(width: 6),
|
||||
itemBuilder: (ctx, i) {
|
||||
if (i < _images.length) {
|
||||
return Stack(children: [
|
||||
InkWell(onTap: () => _showImagePreview(i),
|
||||
child: Container(width: 56, height: 56,
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(6), border: Border.all(color: colors.outlineVariant, width: 0.5)),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: FadeInLocalImage(path: _images[i], fit: BoxFit.cover))),
|
||||
Positioned(top: -4, right: -4,
|
||||
child: GestureDetector(onTap: () => setState(() => _images.removeAt(i)),
|
||||
child: Container(width: 16, height: 16,
|
||||
decoration: BoxDecoration(color: colors.surface, shape: BoxShape.circle, border: Border.all(color: colors.outline)),
|
||||
child: Icon(Icons.close, size: 10, color: colors.onSurface.withValues(alpha: 0.5))))),
|
||||
]);
|
||||
}
|
||||
return InkWell(onTap: _pickImage,
|
||||
child: Container(width: 56, height: 56,
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(6), color: colors.surfaceContainerHighest,
|
||||
border: Border.all(color: colors.outlineVariant)),
|
||||
child: Icon(Icons.add_photo_alternate_outlined, size: 20, color: colors.onSurface.withValues(alpha: 0.3))));
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/// 顶部区域:返回 / 年月日 周几 / 保存按钮
|
||||
Widget _buildHeader(ColorScheme colors, double topPadding) {
|
||||
return Container(
|
||||
@@ -498,11 +618,63 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
return false;
|
||||
}
|
||||
|
||||
/// 离开确认(自动保存后直接返回)
|
||||
/// 离开确认:有内容时弹窗让用户选择,无内容时直接离开
|
||||
Future<bool> _confirmLeave() async {
|
||||
_autoSaveTimer?.cancel();
|
||||
if (_hasContent()) await _autoSave();
|
||||
return true;
|
||||
if (!_hasContent()) return true;
|
||||
|
||||
final result = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (ctx) {
|
||||
final colors = Theme.of(ctx).colorScheme;
|
||||
return AlertDialog(
|
||||
backgroundColor: colors.surface,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
title: const Text('确认离开', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)),
|
||||
content: const Text('内容尚未保存,是否保存后离开?', style: TextStyle(fontSize: 14, height: 1.5)),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, 'discard'),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: colors.error,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
),
|
||||
child: const Text('丢弃'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, 'cancel'),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: colors.onSurface.withValues(alpha: 0.6),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
),
|
||||
child: const Text('继续编辑'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.pop(ctx, 'save'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: colors.primary,
|
||||
foregroundColor: colors.onPrimary,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
),
|
||||
child: const Text('保存'),
|
||||
),
|
||||
],
|
||||
actionsPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (result == 'save') {
|
||||
await _autoSave();
|
||||
return true;
|
||||
} else if (result == 'discard') {
|
||||
return true;
|
||||
}
|
||||
// 'cancel' 或关闭对话框 → 留在页面
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<void> _saveNote() async {
|
||||
@@ -678,7 +850,7 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
|
||||
Widget _buildGridImageItem(int index) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final size = (MediaQuery.of(context).size.width - 16 * 2 - 10 * 2) / 3;
|
||||
final size = ((MediaQuery.of(context).size.width - 16 * 2 - 10 * 2) / 3).clamp(60.0, 120.0);
|
||||
return InkWell(
|
||||
onTap: () => _showImagePreview(index),
|
||||
onLongPress: () => _showDeleteImageDialog(index),
|
||||
@@ -702,7 +874,7 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
/// 添加图片按钮
|
||||
Widget _buildAddImageButton() {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final size = (MediaQuery.of(context).size.width - 16 * 2 - 10 * 2) / 3;
|
||||
final size = ((MediaQuery.of(context).size.width - 16 * 2 - 10 * 2) / 3).clamp(60.0, 120.0);
|
||||
return InkWell(
|
||||
onTap: _pickImage,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
|
||||
@@ -617,16 +617,19 @@ class AppProvider extends ChangeNotifier {
|
||||
/// 添加影评
|
||||
Future<void> addMovieReview(MovieReview review) async {
|
||||
await _reviewDao.insertReview(review);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 更新影评
|
||||
Future<void> updateMovieReview(MovieReview review) async {
|
||||
await _reviewDao.updateReview(review);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 删除影评
|
||||
Future<void> removeMovieReview(String id) async {
|
||||
await _reviewDao.deleteReview(id);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 获取影视的影评数量
|
||||
@@ -644,6 +647,7 @@ class AppProvider extends ChangeNotifier {
|
||||
/// 添加海报
|
||||
Future<void> addMoviePoster(MoviePoster poster) async {
|
||||
await _posterDao.insertPoster(poster);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 删除海报
|
||||
@@ -653,6 +657,7 @@ class AppProvider extends ChangeNotifier {
|
||||
await ImagePathHelper.instance.deleteFile(poster.posterPath);
|
||||
}
|
||||
await _posterDao.deletePoster(id);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 获取影视的海报数量
|
||||
@@ -670,16 +675,19 @@ class AppProvider extends ChangeNotifier {
|
||||
/// 添加游戏评价
|
||||
Future<void> addGameReview(GameReview review) async {
|
||||
await _gameReviewDao.insertReview(review);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 更新游戏评价
|
||||
Future<void> updateGameReview(GameReview review) async {
|
||||
await _gameReviewDao.updateReview(review);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 删除游戏评价
|
||||
Future<void> removeGameReview(String id) async {
|
||||
await _gameReviewDao.deleteReview(id);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 获取游戏的评价数量
|
||||
@@ -697,6 +705,7 @@ class AppProvider extends ChangeNotifier {
|
||||
/// 添加游戏截图
|
||||
Future<void> addGameScreenshot(GameScreenshot screenshot) async {
|
||||
await _gameScreenshotDao.insertScreenshot(screenshot);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 删除游戏截图
|
||||
@@ -706,6 +715,7 @@ class AppProvider extends ChangeNotifier {
|
||||
await ImagePathHelper.instance.deleteFile(screenshot.screenshotPath);
|
||||
}
|
||||
await _gameScreenshotDao.deleteScreenshot(id);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 获取游戏的截图数量
|
||||
@@ -723,16 +733,19 @@ class AppProvider extends ChangeNotifier {
|
||||
/// 添加书评
|
||||
Future<void> addBookReview(BookReview review) async {
|
||||
await _bookReviewDao.insertReview(review);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 更新书评
|
||||
Future<void> updateBookReview(BookReview review) async {
|
||||
await _bookReviewDao.updateReview(review);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 删除书评
|
||||
Future<void> removeBookReview(String id) async {
|
||||
await _bookReviewDao.deleteReview(id);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 获取书籍的书评数量
|
||||
@@ -750,16 +763,19 @@ class AppProvider extends ChangeNotifier {
|
||||
/// 添加摘抄
|
||||
Future<void> addBookExcerpt(BookExcerpt excerpt) async {
|
||||
await _bookExcerptDao.insertExcerpt(excerpt);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 更新摘抄
|
||||
Future<void> updateBookExcerpt(BookExcerpt excerpt) async {
|
||||
await _bookExcerptDao.updateExcerpt(excerpt);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 删除摘抄
|
||||
Future<void> removeBookExcerpt(String id) async {
|
||||
await _bookExcerptDao.deleteExcerpt(id);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 获取书籍的摘抄数量
|
||||
|
||||
@@ -36,6 +36,7 @@ class VditorEditorState extends State<VditorEditor> {
|
||||
InAppWebViewController? _controller;
|
||||
bool _isReady = false;
|
||||
bool _loadFailed = false;
|
||||
late final TextEditingController _fallbackController;
|
||||
String? _distDir; // Windows: 文件系统路径
|
||||
double _contentHeight = 200; // WebView 内容高度,随内容撑开
|
||||
double _lastKeyboardH = 0; // 上次键盘高度,用于检测键盘弹出
|
||||
@@ -48,6 +49,7 @@ class VditorEditorState extends State<VditorEditor> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fallbackController = TextEditingController(text: widget.initialContent ?? '');
|
||||
_startFallbackTimer();
|
||||
if (Platform.isWindows) {
|
||||
_locateDistDir();
|
||||
@@ -87,6 +89,7 @@ class VditorEditorState extends State<VditorEditor> {
|
||||
@override
|
||||
void dispose() {
|
||||
_fallbackTimer?.cancel();
|
||||
_fallbackController.dispose();
|
||||
_destroyVditor();
|
||||
super.dispose();
|
||||
}
|
||||
@@ -218,7 +221,7 @@ class VditorEditorState extends State<VditorEditor> {
|
||||
|
||||
if (_loadFailed) {
|
||||
return TextField(
|
||||
controller: TextEditingController(text: widget.initialContent ?? ''),
|
||||
controller: _fallbackController,
|
||||
maxLines: null,
|
||||
expands: true,
|
||||
textAlignVertical: TextAlignVertical.top,
|
||||
@@ -241,6 +244,28 @@ class VditorEditorState extends State<VditorEditor> {
|
||||
return Center(child: CircularProgressIndicator(strokeWidth: 2, color: colors.primary));
|
||||
}
|
||||
|
||||
// Windows: WebView 环境未初始化时,直接 fallback 到纯文本编辑
|
||||
if (Platform.isWindows && windowsWebViewEnvironment == null) {
|
||||
_loadFailed = true;
|
||||
return TextField(
|
||||
controller: _fallbackController,
|
||||
maxLines: null,
|
||||
expands: true,
|
||||
textAlignVertical: TextAlignVertical.top,
|
||||
strutStyle: const StrutStyle(forceStrutHeight: true, height: 1.6, fontSize: 14),
|
||||
style: TextStyle(fontSize: 14, color: colors.onSurface, height: 1.6),
|
||||
decoration: InputDecoration(
|
||||
hintText: widget.placeholder,
|
||||
hintStyle: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.25), height: 1.6),
|
||||
border: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
contentPadding: const EdgeInsets.all(16),
|
||||
),
|
||||
onChanged: widget.onContentChanged,
|
||||
);
|
||||
}
|
||||
|
||||
final String initialUrl;
|
||||
if (Platform.isWindows) {
|
||||
final htmlPath = p.join(_distDir!, 'vditor_editor.html');
|
||||
@@ -258,6 +283,98 @@ class VditorEditorState extends State<VditorEditor> {
|
||||
}
|
||||
_lastKeyboardH = keyboardH;
|
||||
|
||||
// Windows 桌面端:WebView 填满父容器,自身管理内部滚动
|
||||
// 移动端:使用 _contentHeight 动态撑开,配合 onHeightChanged 回调
|
||||
if (Platform.isWindows) {
|
||||
return Stack(
|
||||
children: [
|
||||
InAppWebView(
|
||||
webViewEnvironment: windowsWebViewEnvironment,
|
||||
initialUrlRequest: URLRequest(url: WebUri(initialUrl)),
|
||||
initialSettings: InAppWebViewSettings(
|
||||
javaScriptEnabled: true,
|
||||
transparentBackground: true,
|
||||
disableContextMenu: false,
|
||||
useHybridComposition: true,
|
||||
allowFileAccessFromFileURLs: true,
|
||||
allowUniversalAccessFromFileURLs: true,
|
||||
),
|
||||
onWebViewCreated: (controller) {
|
||||
_controller = controller;
|
||||
controller.addJavaScriptHandler(
|
||||
handlerName: 'onVditorReady',
|
||||
callback: (_) => _onVditorReady(),
|
||||
);
|
||||
controller.addJavaScriptHandler(
|
||||
handlerName: 'onContentChanged',
|
||||
callback: (args) {
|
||||
if (args.isNotEmpty) {
|
||||
widget.onContentChanged?.call(args[0].toString());
|
||||
}
|
||||
},
|
||||
);
|
||||
controller.addJavaScriptHandler(
|
||||
handlerName: 'onPickImage',
|
||||
callback: (_) => _pickImage(),
|
||||
);
|
||||
controller.addJavaScriptHandler(
|
||||
handlerName: 'onImageUpload',
|
||||
callback: (args) {
|
||||
if (args.length >= 3) {
|
||||
_handleImageUpload(args[0].toString(), args[1].toString(), args[2].toString());
|
||||
}
|
||||
},
|
||||
);
|
||||
controller.addJavaScriptHandler(
|
||||
handlerName: 'onHeightChanged',
|
||||
callback: (args) {
|
||||
if (args.isNotEmpty) {
|
||||
final h = double.tryParse(args[0].toString()) ?? _contentHeight;
|
||||
if ((h - _contentHeight).abs() > 2 && h > 0) {
|
||||
setState(() => _contentHeight = h);
|
||||
widget.onHeightChanged?.call(h);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
onLoadStop: (controller, url) async {
|
||||
final theme = widget.isDark ? 'dark' : 'light';
|
||||
final escapedPlaceholder = jsonEncode(widget.placeholder);
|
||||
await controller.evaluateJavascript(
|
||||
source: 'initVditor("$theme", $escapedPlaceholder)',
|
||||
);
|
||||
},
|
||||
onReceivedError: (controller, request, error) {
|
||||
debugPrint('[VditorEditor] load error: ${error.description}');
|
||||
},
|
||||
),
|
||||
// 加载动画:Vditor 就绪前显示
|
||||
if (!_isReady)
|
||||
Container(
|
||||
color: colors.surface,
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(strokeWidth: 2, color: colors.primary),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
'编辑器加载中...',
|
||||
style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.4)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return SizedBox(
|
||||
height: _contentHeight,
|
||||
child: Stack(
|
||||
|
||||
Reference in New Issue
Block a user