generated from dellevin/template
代码优化,功能新增
This commit is contained in:
@@ -30,6 +30,7 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
|
||||
return Scaffold(
|
||||
backgroundColor: colors.surface,
|
||||
appBar: AppBar(
|
||||
titleSpacing: 0,
|
||||
title: Text(
|
||||
note.title.isNotEmpty
|
||||
? note.title
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:io';
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
@@ -9,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';
|
||||
|
||||
/// 添加/编辑笔记页面 - 极简书写界面
|
||||
class NoteFormPage extends StatefulWidget {
|
||||
@@ -30,6 +32,9 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
final ImagePicker _picker = ImagePicker();
|
||||
String? _tempNoteId; // 新建模式时使用的临时笔记ID
|
||||
String _editorMode = 'edit'; // 'edit' | 'preview'
|
||||
Timer? _autoSaveTimer;
|
||||
String _saveStatus = ''; // '', 'saved'
|
||||
Note? _savedNote; // 新建模式首次自动保存后的笔记引用
|
||||
|
||||
static const _weekdays = ['一', '二', '三', '四', '五', '六', '日'];
|
||||
|
||||
@@ -44,15 +49,82 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
_tags = note != null ? List.from(note.tags) : [];
|
||||
_images = note != null ? List.from(note.images) : [];
|
||||
_isEditing = note != null;
|
||||
_titleController.addListener(_onTextChanged);
|
||||
_contentController.addListener(_onTextChanged);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_autoSaveTimer?.cancel();
|
||||
_titleController.dispose();
|
||||
_contentController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onTextChanged() {
|
||||
_autoSaveTimer?.cancel();
|
||||
_autoSaveTimer = Timer(const Duration(seconds: 2), () {
|
||||
if (mounted) _autoSave();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _autoSave() async {
|
||||
final content = _contentController.text.trim();
|
||||
final title = _titleController.text.trim();
|
||||
if (title.isEmpty && content.isEmpty) return;
|
||||
|
||||
try {
|
||||
final now = DateTime.now();
|
||||
if (_isEditing) {
|
||||
final updatedNote = widget.note!.copyWith(
|
||||
title: title,
|
||||
content: content,
|
||||
tags: _tags,
|
||||
images: _images,
|
||||
updatedAt: now,
|
||||
);
|
||||
await context.read<AppProvider>().updateNote(updatedNote);
|
||||
} else if (_savedNote != null) {
|
||||
final updatedNote = _savedNote!.copyWith(
|
||||
title: title,
|
||||
content: content,
|
||||
tags: _tags,
|
||||
images: _images,
|
||||
updatedAt: now,
|
||||
);
|
||||
await context.read<AppProvider>().updateNote(updatedNote);
|
||||
_savedNote = updatedNote;
|
||||
} else {
|
||||
final noteId = now.millisecondsSinceEpoch.toString();
|
||||
List<String> finalImages = [];
|
||||
if (_images.isNotEmpty) {
|
||||
final oldNoteId = _tempNoteId ?? noteId;
|
||||
finalImages = await _moveImagesToNewId(oldNoteId, noteId);
|
||||
}
|
||||
final newNote = Note(
|
||||
id: noteId,
|
||||
title: title,
|
||||
content: content,
|
||||
tags: _tags,
|
||||
images: finalImages.isNotEmpty ? finalImages : _images,
|
||||
createdAt: _createdAt,
|
||||
updatedAt: now,
|
||||
);
|
||||
await context.read<AppProvider>().addNote(newNote);
|
||||
_savedNote = newNote;
|
||||
_isEditing = true;
|
||||
}
|
||||
if (mounted) {
|
||||
setState(() => _saveStatus = 'saved');
|
||||
Timer(const Duration(seconds: 3), () {
|
||||
if (mounted && _saveStatus == 'saved') setState(() => _saveStatus = '');
|
||||
});
|
||||
}
|
||||
} catch (_) {
|
||||
// 自动保存失败静默处理
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
@@ -149,6 +221,19 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
style: TextStyle(fontSize: 17, fontWeight: FontWeight.w600, color: colors.onSurface),
|
||||
),
|
||||
),
|
||||
if (_saveStatus == 'saved')
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(width: 6, height: 6,
|
||||
decoration: BoxDecoration(color: Colors.green, shape: BoxShape.circle)),
|
||||
const SizedBox(width: 4),
|
||||
Text('已保存', style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||
],
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: _saveNote,
|
||||
child: Container(
|
||||
@@ -501,7 +586,7 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
Widget _buildAddTagButton() {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return GestureDetector(
|
||||
onTap: _showAddTagDialog,
|
||||
onTap: _showTagPanel,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
@@ -520,184 +605,26 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 显示添加标签对话框
|
||||
Future<void> _showAddTagDialog() async {
|
||||
final controller = TextEditingController();
|
||||
|
||||
// 从 tags 表获取已有标签
|
||||
/// 显示标签侧边面板
|
||||
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);
|
||||
}
|
||||
// 过滤掉已添加的标签
|
||||
final availableTags = allTags.where((tag) => !_tags.contains(tag)).toList()..sort();
|
||||
|
||||
showDialog(
|
||||
if (!mounted) return;
|
||||
TagSidePanel.show(
|
||||
context: context,
|
||||
builder: (ctx) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return StatefulBuilder(
|
||||
builder: (ctx, setDialogState) => AlertDialog(
|
||||
backgroundColor: colors.surface,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
titlePadding: const EdgeInsets.fromLTRB(24, 24, 24, 0),
|
||||
title: Text(
|
||||
'添加标签',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: colors.onSurface,
|
||||
),
|
||||
),
|
||||
contentPadding: const EdgeInsets.fromLTRB(24, 16, 24, 0),
|
||||
content: SizedBox(
|
||||
width: double.maxFinite,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 输入框
|
||||
TextField(
|
||||
controller: controller,
|
||||
autofocus: true,
|
||||
style: TextStyle(fontSize: 14, color: colors.onSurface),
|
||||
cursorColor: colors.primary,
|
||||
decoration: InputDecoration(
|
||||
hintText: '输入新标签名称',
|
||||
hintStyle: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.3)),
|
||||
filled: true,
|
||||
fillColor: colors.surfaceContainerHigh,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide(color: colors.primary, width: 1),
|
||||
),
|
||||
suffixIcon: controller.text.isNotEmpty
|
||||
? IconButton(
|
||||
icon: Icon(Icons.clear, size: 16, color: colors.onSurface.withValues(alpha: 0.35)),
|
||||
onPressed: () => controller.clear(),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
onChanged: (_) => setDialogState(() {}),
|
||||
onSubmitted: (value) {
|
||||
_addTag(value);
|
||||
controller.clear();
|
||||
setDialogState(() {});
|
||||
},
|
||||
),
|
||||
|
||||
// 已有标签列表
|
||||
if (availableTags.isNotEmpty) ...[
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
'或选择已有标签',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: colors.onSurface.withValues(alpha: 0.35),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 180),
|
||||
child: SingleChildScrollView(
|
||||
child: Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: availableTags.map((tag) {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
_addTag(tag);
|
||||
Navigator.pop(ctx);
|
||||
},
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: colors.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: colors.outline, width: 0.5),
|
||||
),
|
||||
child: Text(
|
||||
tag,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: colors.onSurface.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
if (availableTags.isEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 16, bottom: 8),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'暂无已有标签',
|
||||
style: TextStyle(fontSize: 13, color: Colors.grey[400]),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: colors.onSurface.withValues(alpha: 0.4),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
),
|
||||
child: const Text('取消', style: TextStyle(fontSize: 14)),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
_addTag(controller.text);
|
||||
Navigator.pop(ctx);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: colors.primary,
|
||||
foregroundColor: colors.onPrimary,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
|
||||
),
|
||||
child: const Text('添加', style: TextStyle(fontSize: 14)),
|
||||
),
|
||||
],
|
||||
actionsPadding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||
),
|
||||
);
|
||||
selectedTags: List.from(_tags),
|
||||
allAvailableTags: allTags.toList()..sort(),
|
||||
onTagsChanged: (newTags) {
|
||||
setState(() => _tags = newTags);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 获取所有已有标签(从所有笔记中收集)
|
||||
/// 添加标签
|
||||
void _addTag(String tag) {
|
||||
final trimmed = tag.trim();
|
||||
if (trimmed.isNotEmpty && !_tags.contains(trimmed)) {
|
||||
setState(() => _tags.add(trimmed));
|
||||
}
|
||||
}
|
||||
|
||||
/// 标题输入行
|
||||
Widget _buildTitleInput(ColorScheme colors) {
|
||||
return Padding(
|
||||
@@ -729,42 +656,15 @@ class _NoteFormPageState extends State<NoteFormPage> {
|
||||
return false;
|
||||
}
|
||||
|
||||
/// 离开确认
|
||||
/// 离开确认(自动保存后直接返回)
|
||||
Future<bool> _confirmLeave() async {
|
||||
if (!_hasContent()) return true;
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final result = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
backgroundColor: colors.surface,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
title: Text('未保存', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||
content: Text('当前内容未保存,确定要离开吗?',
|
||||
style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6), height: 1.5)),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, false),
|
||||
child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6))),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.pop(ctx, true),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: colors.error,
|
||||
foregroundColor: colors.onError,
|
||||
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),
|
||||
),
|
||||
);
|
||||
return result ?? false;
|
||||
_autoSaveTimer?.cancel();
|
||||
if (_hasContent()) await _autoSave();
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<void> _saveNote() async {
|
||||
_autoSaveTimer?.cancel();
|
||||
final content = _contentController.text.trim();
|
||||
final title = _titleController.text.trim();
|
||||
|
||||
|
||||
@@ -24,10 +24,7 @@ class _NoteTabPageState extends State<NoteTabPage> {
|
||||
AppProvider? _provider;
|
||||
int _layoutStyle = 0;
|
||||
bool _initialized = false;
|
||||
int _lastDataCount = -1;
|
||||
DateTime? _lastUpdatedAt;
|
||||
int _lastScrollSignal = 0;
|
||||
String? _selectedTag;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -38,8 +35,6 @@ class _NoteTabPageState extends State<NoteTabPage> {
|
||||
final provider = context.read<AppProvider>();
|
||||
_provider = provider;
|
||||
provider.addListener(_onDataChanged);
|
||||
_lastDataCount = provider.notes.length;
|
||||
if (provider.notes.isNotEmpty) _lastUpdatedAt = provider.notes.first.updatedAt;
|
||||
_loadFirst();
|
||||
});
|
||||
}
|
||||
@@ -55,7 +50,6 @@ class _NoteTabPageState extends State<NoteTabPage> {
|
||||
if (!_initialized || !mounted) return;
|
||||
final provider = context.read<AppProvider>();
|
||||
|
||||
// 检查回到顶部信号
|
||||
if (provider.scrollToTopSignal != _lastScrollSignal && provider.scrollToTopSignal > 0) {
|
||||
_lastScrollSignal = provider.scrollToTopSignal;
|
||||
if (_scrollController.hasClients) {
|
||||
@@ -63,15 +57,8 @@ class _NoteTabPageState extends State<NoteTabPage> {
|
||||
}
|
||||
}
|
||||
|
||||
final count = provider.notes.length;
|
||||
final latest = provider.notes.isNotEmpty ? provider.notes.first.updatedAt : null;
|
||||
if (count != _lastDataCount || latest != _lastUpdatedAt) {
|
||||
_lastDataCount = count;
|
||||
_lastUpdatedAt = latest;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) _loadFirst();
|
||||
});
|
||||
}
|
||||
// 数据变化时刷新列表(排序/评分/新增等)
|
||||
_loadFirst();
|
||||
}
|
||||
|
||||
void _onScroll() {
|
||||
@@ -83,12 +70,8 @@ class _NoteTabPageState extends State<NoteTabPage> {
|
||||
Future<void> _loadFirst() async {
|
||||
_initialized = true;
|
||||
setState(() { _isLoading = true; _offset = 0; _hasMore = true; });
|
||||
List<Note> list = await context.read<AppProvider>().loadNotesPaged(offset: 0);
|
||||
if (_selectedTag != null) {
|
||||
final all = context.read<AppProvider>().notes.where((n) => !n.isDeleted && n.tags.contains(_selectedTag)).toList()
|
||||
..sort((a, b) => b.updatedAt.compareTo(a.updatedAt));
|
||||
list = all.take(20).toList();
|
||||
}
|
||||
final sortMode = UserPrefs().noteSortMode;
|
||||
final list = await context.read<AppProvider>().loadNotesPaged(offset: 0, sortMode: sortMode);
|
||||
if (!mounted) return;
|
||||
setState(() { _items.clear(); _items.addAll(list); _offset = list.length; _hasMore = list.length >= 20; _isLoading = false; });
|
||||
}
|
||||
@@ -96,14 +79,8 @@ class _NoteTabPageState extends State<NoteTabPage> {
|
||||
Future<void> _loadMore() async {
|
||||
if (_isLoading || !_hasMore) return;
|
||||
setState(() => _isLoading = true);
|
||||
List<Note> list;
|
||||
if (_selectedTag != null) {
|
||||
final all = context.read<AppProvider>().notes.where((n) => !n.isDeleted && n.tags.contains(_selectedTag)).toList()
|
||||
..sort((a, b) => b.updatedAt.compareTo(a.updatedAt));
|
||||
list = all.skip(_offset).take(20).toList();
|
||||
} else {
|
||||
list = await context.read<AppProvider>().loadNotesPaged(offset: _offset);
|
||||
}
|
||||
final sortMode = UserPrefs().noteSortMode;
|
||||
final list = await context.read<AppProvider>().loadNotesPaged(offset: _offset, sortMode: sortMode);
|
||||
if (!mounted) return;
|
||||
setState(() { _items.addAll(list); _offset += list.length; _hasMore = list.length >= 20; _isLoading = false; });
|
||||
}
|
||||
@@ -118,20 +95,11 @@ class _NoteTabPageState extends State<NoteTabPage> {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Consumer<AppProvider>(builder: (context, provider, _) {
|
||||
if (_items.isEmpty && _isLoading) return _buildSkeleton();
|
||||
if (_items.isEmpty && _selectedTag == null) {
|
||||
if (_items.isEmpty) {
|
||||
return RefreshIndicator(onRefresh: _refresh, color: colors.primary, backgroundColor: colors.surface,
|
||||
child: ListView(physics: const AlwaysScrollableScrollPhysics(), children: [_buildEmptyState(context)]));
|
||||
}
|
||||
return Column(
|
||||
children: [
|
||||
_buildTagBar(provider),
|
||||
Expanded(
|
||||
child: _items.isEmpty
|
||||
? Center(child: Text('没有"$_selectedTag"相关的笔记', style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.4))))
|
||||
: _buildContent(),
|
||||
),
|
||||
],
|
||||
);
|
||||
return _buildContent();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -141,53 +109,6 @@ class _NoteTabPageState extends State<NoteTabPage> {
|
||||
return _buildListView();
|
||||
}
|
||||
|
||||
Widget _buildTagBar(AppProvider provider) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final allTags = <String>{};
|
||||
for (final n in provider.notes.where((n) => !n.isDeleted)) {
|
||||
allTags.addAll(n.tags);
|
||||
}
|
||||
if (allTags.isEmpty) return const SizedBox.shrink();
|
||||
final tags = allTags.toList()..sort();
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(bottom: BorderSide(color: colors.outlineVariant, width: 0.5)),
|
||||
),
|
||||
child: SizedBox(
|
||||
height: 32,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
itemCount: tags.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(width: 6),
|
||||
itemBuilder: (_, i) {
|
||||
final tag = tags[i];
|
||||
final selected = _selectedTag == tag;
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
setState(() => _selectedTag = selected ? null : tag);
|
||||
_loadFirst();
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: selected ? colors.primary : colors.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(tag, style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: selected ? colors.onPrimary : colors.onSurface.withValues(alpha: 0.6),
|
||||
)),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSkeleton() {
|
||||
switch (_layoutStyle) {
|
||||
case 1: return _buildWaterfallSkeleton();
|
||||
@@ -246,7 +167,7 @@ class _NoteTabPageState extends State<NoteTabPage> {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return GestureDetector(
|
||||
onTap: () => Navigator.pushNamed(context, '/note-detail', arguments: note).then((_) => _loadFirst()),
|
||||
onLongPress: () => _showDeleteDialog(context, note),
|
||||
onLongPress: () => _showNoteActions(note),
|
||||
child: IntrinsicHeight(child: Row(crossAxisAlignment: CrossAxisAlignment.stretch, children: [
|
||||
SizedBox(width: 40, child: Column(children: [
|
||||
Container(width: 10, height: 10,
|
||||
@@ -256,7 +177,10 @@ class _NoteTabPageState extends State<NoteTabPage> {
|
||||
Expanded(child: Container(margin: const EdgeInsets.only(bottom: 16), padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(color: colors.surfaceContainerHigh, borderRadius: BorderRadius.circular(12)),
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [
|
||||
Text(_formatFullDate(note.updatedAt), style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||
Row(children: [
|
||||
Expanded(child: Text(_formatFullDate(note.updatedAt), style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.4)))),
|
||||
if (note.isPinned) Icon(Icons.push_pin, size: 14, color: colors.primary),
|
||||
]),
|
||||
if (note.title.isNotEmpty) ...[const SizedBox(height: 6),
|
||||
Text(note.title, style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: colors.onSurface), maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
],
|
||||
@@ -305,7 +229,7 @@ class _NoteTabPageState extends State<NoteTabPage> {
|
||||
final extraCount = images.length - 1;
|
||||
return GestureDetector(
|
||||
onTap: () => Navigator.pushNamed(context, '/note-detail', arguments: note).then((_) => _loadFirst()),
|
||||
onLongPress: () => _showDeleteDialog(context, note),
|
||||
onLongPress: () => _showNoteActions(note),
|
||||
child: Container(margin: const EdgeInsets.only(bottom: 8),
|
||||
decoration: BoxDecoration(color: colors.surface, borderRadius: BorderRadius.circular(10),
|
||||
boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.04), blurRadius: 6, offset: const Offset(0, 2))]),
|
||||
@@ -344,9 +268,7 @@ class _NoteTabPageState extends State<NoteTabPage> {
|
||||
]),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _getPreviewText(Note note) {
|
||||
} String _getPreviewText(Note note) {
|
||||
final text = note.content.replaceAll(RegExp(r'[#*\[\]\(\)]'), '').trim();
|
||||
return text.isEmpty ? '(无内容)' : text;
|
||||
}
|
||||
@@ -368,23 +290,72 @@ class _NoteTabPageState extends State<NoteTabPage> {
|
||||
);
|
||||
}
|
||||
|
||||
void _showDeleteDialog(BuildContext context, Note note) {
|
||||
void _showNoteActions(Note note) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
showDialog(context: context, builder: (ctx) => AlertDialog(
|
||||
backgroundColor: colors.surface, elevation: 0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
title: Text('确认删除', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||
content: Text('确定要删除这条笔记吗?删除后可在回收站恢复。',
|
||||
style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6), height: 1.5)),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(ctx), child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6)))),
|
||||
ElevatedButton(onPressed: () async { await context.read<AppProvider>().removeNote(note.id); Navigator.pop(ctx); _loadFirst(); },
|
||||
style: ElevatedButton.styleFrom(backgroundColor: colors.error, foregroundColor: colors.onError, 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),
|
||||
));
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: colors.surface,
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(16))),
|
||||
builder: (ctx) => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
Container(width: 36, height: 4, decoration: BoxDecoration(color: colors.onSurface.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(2))),
|
||||
const SizedBox(height: 16),
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: Container(width: 36, height: 36, decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)),
|
||||
child: Icon(note.isPinned ? Icons.push_pin_outlined : Icons.push_pin, size: 20, color: colors.onSurface.withValues(alpha: 0.6))),
|
||||
title: Text(note.isPinned ? '取消置顶' : '置顶', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: colors.onSurface)),
|
||||
subtitle: Text(note.isPinned ? '取消置顶后按时间排序' : '置顶后始终显示在最前', style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||
trailing: Icon(Icons.chevron_right, color: colors.onSurface.withValues(alpha: 0.25)),
|
||||
onTap: () {
|
||||
Navigator.pop(ctx);
|
||||
context.read<AppProvider>().toggleNotePin(note.id, !note.isPinned);
|
||||
},
|
||||
),
|
||||
Divider(height: 0.5, color: colors.outlineVariant),
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: Container(width: 36, height: 36, decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)),
|
||||
child: Icon(Icons.delete_outline, size: 20, color: colors.error)),
|
||||
title: Text('删除', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: colors.error)),
|
||||
subtitle: Text('删除后可在回收站恢复', style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.4))),
|
||||
trailing: Icon(Icons.chevron_right, color: colors.onSurface.withValues(alpha: 0.25)),
|
||||
onTap: () {
|
||||
Navigator.pop(ctx);
|
||||
_showDeleteDialog(note);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
]),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showDeleteDialog(Note note) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
backgroundColor: colors.surface, elevation: 0,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
title: Text('确认删除', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)),
|
||||
content: Text('确定要删除这条笔记吗?删除后可在回收站恢复。',
|
||||
style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6), height: 1.5)),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(ctx),
|
||||
child: Text('取消', style: TextStyle(color: colors.onSurface.withValues(alpha: 0.6)))),
|
||||
ElevatedButton(
|
||||
onPressed: () async { await context.read<AppProvider>().removeNote(note.id); Navigator.pop(ctx); _loadFirst(); },
|
||||
style: ElevatedButton.styleFrom(backgroundColor: colors.error, foregroundColor: colors.onError, 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),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyState(BuildContext context) {
|
||||
|
||||
Reference in New Issue
Block a user