add long-press gesture to open note scrolled to end

Long-pressing a note in the list opens it in view mode with the scroll
position at the bottom of the file. Switching to edit mode the first
time positions the cursor at the end of the text and scrolls there too,
so the user can immediately append content.

Implementation:
- NoteEditorScreen gains a scrollToEnd parameter (default false)
- A ScrollController is attached to the view/edit SingleChildScrollView
- After content loads with scrollToEnd=true, two post-frame callbacks
  fire (first renders the content, second has a stable maxScrollExtent)
  then jumpTo(maxScrollExtent)
- _cursorAtEnd flag is consumed on the first Mode Edit switch: sets
  selection to text.length, requests focus, and scrolls to bottom
- NoteListScreen ListTile gets onLongPress that navigates with
  scrollToEnd: true and reloads on return

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
randogoth 2026-06-19 17:50:56 +03:00
parent f91484d6d4
commit db13702e54
2 changed files with 42 additions and 2 deletions

View file

@ -14,10 +14,10 @@ import 'codefield.dart';
class NoteEditorScreen extends StatefulWidget {
final File note;
final String? notesDirectoryPath; // Add this
final String? notesDirectoryPath;
final bool isDarkMode;
final NoteMode initialMode;
final bool scrollToEnd;
const NoteEditorScreen({
super.key,
@ -25,6 +25,7 @@ class NoteEditorScreen extends StatefulWidget {
required this.isDarkMode,
required this.notesDirectoryPath,
this.initialMode = NoteMode.view,
this.scrollToEnd = false,
});
@override
@ -38,14 +39,17 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
late String _initialContent;
late NoteMode _mode;
final FocusNode _editorFocusNode = FocusNode();
final ScrollController _scrollController = ScrollController();
bool _isLoading = true;
bool _autoSaveEnabled = false;
double _fontSize = 16.0;
bool _cursorAtEnd = false;
@override
void initState() {
super.initState();
_mode = widget.initialMode;
_cursorAtEnd = widget.scrollToEnd;
_loadPreferences();
_loadNoteContent();
}
@ -88,6 +92,17 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
TextSelection.collapsed(offset: _controller.text.length);
_editorFocusNode.requestFocus();
});
} else if (widget.scrollToEnd) {
// Two frames: first renders the content, second has a stable maxScrollExtent.
WidgetsBinding.instance.addPostFrameCallback((_) {
WidgetsBinding.instance.addPostFrameCallback((_) => _scrollToBottom());
});
}
}
void _scrollToBottom() {
if (_scrollController.hasClients) {
_scrollController.jumpTo(_scrollController.position.maxScrollExtent);
}
}
@ -160,6 +175,7 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
_controller.removeListener(_onTextChanged);
_controller.dispose();
_editorFocusNode.dispose();
_scrollController.dispose();
super.dispose();
}
@ -208,6 +224,15 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
break;
case 'Mode Edit':
setState(() { _mode = NoteMode.edit; });
if (_cursorAtEnd) {
_cursorAtEnd = false; // consume once
WidgetsBinding.instance.addPostFrameCallback((_) {
_controller.selection = TextSelection.collapsed(
offset: _controller.text.length);
_editorFocusNode.requestFocus();
_scrollToBottom();
});
}
break;
case 'Mode Preview':
setState(() { _mode = NoteMode.preview; });
@ -362,6 +387,7 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
data: CodeThemeData(
styles: widget.isDarkMode ? nightTheme : dayTheme),
child: SingleChildScrollView(
controller: _scrollController,
child: WrappedCodeField(
wrap: true,
controller: _controller,

View file

@ -943,6 +943,20 @@ Happy note taking! ✨
setState(() {}); // Force UI refresh
}
},
onLongPress: () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => NoteEditorScreen(
note: noteFile,
isDarkMode: widget.isDarkMode,
notesDirectoryPath: notesDirectoryPath,
scrollToEnd: true,
),
),
);
if (context.mounted) _loadNotes();
},
),
);
},