import 'package:flutter/material.dart'; import 'package:lucide_icons/lucide_icons.dart'; import 'package:flutter_markdown_plus/flutter_markdown_plus.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../core/exceptions.dart'; import '../../core/theme.dart'; import '../../core/wikilink_syntax.dart'; import '../../providers/api_client_provider.dart'; import '../../providers/notes_provider.dart'; import '../../widgets/project_selector.dart'; class NoteEditScreen extends ConsumerStatefulWidget { final int? noteId; final String? noteType; // passed when creating a typed note from KnowledgeScreen const NoteEditScreen({super.key, this.noteId, this.noteType}); @override ConsumerState createState() => _NoteEditScreenState(); } class _NoteEditScreenState extends ConsumerState { final _titleController = TextEditingController(); final _contentController = TextEditingController(); final _tagController = TextEditingController(); List _tags = []; int? _projectId; bool _preview = false; bool _saving = false; late String _noteType; late final Future _initFuture; @override void initState() { super.initState(); _noteType = widget.noteType ?? 'note'; _initFuture = widget.noteId != null ? _loadExisting() : Future.value(); } @override void dispose() { _titleController.dispose(); _contentController.dispose(); _tagController.dispose(); super.dispose(); } Future _loadExisting() async { final note = await ref.read(notesRepositoryProvider).getOne(widget.noteId!); _titleController.text = note.title; _contentController.text = note.body; _tags = List.from(note.tags); _projectId = note.projectId; _noteType = note.noteType; } void _addTag(String raw) { final tag = raw.trim().replaceAll(',', '').toLowerCase(); if (tag.isEmpty || _tags.contains(tag)) { _tagController.clear(); return; } setState(() { _tags = [..._tags, tag]; _tagController.clear(); }); } void _removeTag(String tag) { setState(() => _tags = _tags.where((t) => t != tag).toList()); } Future _delete() async { final confirm = await showDialog( context: context, builder: (dialogContext) => AlertDialog( title: const Text('Delete note?'), content: Text(_titleController.text), actions: [ TextButton( onPressed: () => Navigator.pop(dialogContext, false), child: const Text('Cancel'), ), TextButton( onPressed: () => Navigator.pop(dialogContext, true), style: TextButton.styleFrom( foregroundColor: Theme.of(dialogContext).extension()!.destructive, ), child: const Text('Delete'), ), ], ), ); if (confirm == true) { await ref.read(notesProvider.notifier).delete(widget.noteId!); if (mounted) context.pop(); } } Future _save() async { final title = _titleController.text.trim(); final body = _contentController.text; if (title.isEmpty) { ScaffoldMessenger.of(context) .showSnackBar(const SnackBar(content: Text('Title is required.'))); return; } setState(() => _saving = true); try { if (widget.noteId == null) { await ref.read(notesProvider.notifier).create( title, body, tags: _tags, projectId: _projectId, noteType: _noteType, ); if (mounted) context.pop(); } else { await ref.read(notesProvider.notifier).updateNote( widget.noteId!, title, body, tags: _tags, projectId: _projectId, clearProject: _projectId == null, noteType: _noteType, ); if (mounted) context.pop(); } } on AppException catch (e) { if (mounted) { ScaffoldMessenger.of(context) .showSnackBar(SnackBar(content: Text(e.message))); } } finally { if (mounted) setState(() => _saving = false); } } @override Widget build(BuildContext context) { return FutureBuilder( future: _initFuture, builder: (context, snapshot) { return Scaffold( appBar: AppBar( title: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(widget.noteId == null ? 'New Note' : 'Edit Note'), if (_noteType != 'note') Chip( label: Text( _noteType, style: const TextStyle(fontSize: 11), ), padding: EdgeInsets.zero, visualDensity: VisualDensity.compact, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, ), ], ), actions: [ if (widget.noteId != null) IconButton( icon: const Icon(LucideIcons.trash2), tooltip: 'Delete', onPressed: _delete, ), IconButton( icon: Icon(_preview ? LucideIcons.pencil : LucideIcons.eye), tooltip: _preview ? 'Edit' : 'Preview', onPressed: () => setState(() => _preview = !_preview), ), IconButton( icon: _saving ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(LucideIcons.check), onPressed: _saving ? null : _save, ), ], ), body: snapshot.connectionState == ConnectionState.waiting ? const Center(child: CircularProgressIndicator()) : Column( children: [ Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 0), child: TextField( controller: _titleController, decoration: const InputDecoration( hintText: 'Title', border: InputBorder.none, ), style: Theme.of(context).textTheme.titleLarge, textInputAction: TextInputAction.next, ), ), Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), child: _TagInput( tags: _tags, controller: _tagController, onAdd: _addTag, onRemove: _removeTag, ), ), Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), child: ProjectSelector( value: _projectId, onChanged: (id) => setState(() => _projectId = id), ), ), const Divider(height: 1), Expanded( child: _preview ? Markdown( data: _contentController.text, extensionSet: wikilinkExtensionSet, ) : Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: TextField( controller: _contentController, decoration: const InputDecoration( hintText: 'Write in markdown...', border: InputBorder.none, ), maxLines: null, expands: true, keyboardType: TextInputType.multiline, textAlignVertical: TextAlignVertical.top, ), ), ), ], ), ); }, ); } } class _TagInput extends StatelessWidget { final List tags; final TextEditingController controller; final ValueChanged onAdd; final ValueChanged onRemove; const _TagInput({ required this.tags, required this.controller, required this.onAdd, required this.onRemove, }); @override Widget build(BuildContext context) { return Wrap( spacing: 6, runSpacing: 4, crossAxisAlignment: WrapCrossAlignment.center, children: [ ...tags.map( (tag) => Chip( label: Text('#$tag', style: const TextStyle(fontSize: 12)), materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, padding: const EdgeInsets.symmetric(horizontal: 4), deleteIcon: const Icon(LucideIcons.x, size: 14), onDeleted: () => onRemove(tag), ), ), SizedBox( width: 120, child: TextField( controller: controller, decoration: const InputDecoration( hintText: 'Add tag…', border: InputBorder.none, isDense: true, contentPadding: EdgeInsets.symmetric(vertical: 4), ), style: const TextStyle(fontSize: 13), textInputAction: TextInputAction.done, onSubmitted: onAdd, onChanged: (v) { if (v.endsWith(',') || v.endsWith(' ')) { onAdd(v.replaceAll(RegExp(r'[, ]+$'), '')); } }, ), ), ], ); } }